Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e033caddff
commit
a9b8a31684
|
|
@ -1251,3 +1251,7 @@ Tailwind/StringInterpolation:
|
|||
- '{,ee/,jh/}app/helpers/**/*.rb'
|
||||
- '{,ee/,jh/}app/components/**/*.{haml,rb}'
|
||||
- '{,ee/,jh/}app/views/**/*.haml'
|
||||
|
||||
Cop/ActiveRecordDependent:
|
||||
Include:
|
||||
- ee/app/models/**/*.rb
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Ci
|
||||
class NamespaceSettingsUpdate < BaseMutation
|
||||
graphql_name 'NamespaceSettingsUpdate'
|
||||
|
||||
include ResolvesNamespace
|
||||
|
||||
authorize :maintainer_access
|
||||
|
||||
argument :full_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full path of the namespace the settings belong to.'
|
||||
argument :pipeline_variables_default_role, ::Types::Ci::PipelineVariablesDefaultRoleTypeEnum,
|
||||
required: false,
|
||||
description: copy_field_description(Types::Ci::NamespaceSettingsType, :pipeline_variables_default_role)
|
||||
|
||||
field :ci_cd_settings,
|
||||
Types::Ci::NamespaceSettingsType,
|
||||
null: false,
|
||||
description: 'Namespace CI/CD settings after mutation.'
|
||||
|
||||
def resolve(full_path:, **args)
|
||||
namespace = authorized_find!(full_path)
|
||||
settings = namespace.namespace_settings
|
||||
|
||||
service_response = ::Ci::NamespaceSettings::UpdateService
|
||||
.new(settings, args)
|
||||
.execute
|
||||
|
||||
{
|
||||
ci_cd_settings: settings,
|
||||
errors: service_response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path)
|
||||
resolve_namespace(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Todos
|
||||
# Base class for bulk todo operations
|
||||
|
||||
class BaseMany < ::Mutations::BaseMutation # rubocop:disable GraphQL/GraphqlName -- Base class needs no name.
|
||||
MAX_UPDATE_AMOUNT = 100
|
||||
|
||||
argument :ids,
|
||||
[::Types::GlobalIDType[::Todo]],
|
||||
required: true,
|
||||
description: 'Global IDs of the to-do items to process (a maximum of 100 is supported at once).'
|
||||
|
||||
field :todos, [::Types::TodoType],
|
||||
null: false,
|
||||
description: 'Updated to-do items.'
|
||||
|
||||
def resolve(ids:)
|
||||
check_update_amount_limit!(ids)
|
||||
|
||||
todos = authorized_find_all_pending_by_current_user(model_ids_of(ids))
|
||||
updated_ids = process_todos(todos)
|
||||
|
||||
{
|
||||
updated_ids: updated_ids,
|
||||
todos: Todo.id_in(updated_ids),
|
||||
errors: errors_on_objects(todos)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_todos(todos)
|
||||
raise NotImplementedError, "#{self.class} must implement #process_todos"
|
||||
end
|
||||
|
||||
def todo_state_to_find
|
||||
raise NotImplementedError, "#{self.class} must implement #todo_state_to_find"
|
||||
end
|
||||
|
||||
def model_ids_of(ids)
|
||||
ids.filter_map { |gid| gid.model_id.to_i }
|
||||
end
|
||||
|
||||
def raise_too_many_todos_requested_error
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, 'Too many to-do items requested.'
|
||||
end
|
||||
|
||||
def check_update_amount_limit!(ids)
|
||||
raise_too_many_todos_requested_error if ids.size > MAX_UPDATE_AMOUNT
|
||||
end
|
||||
|
||||
def errors_on_objects(todos)
|
||||
todos.flat_map { |todo| errors_on_object(todo) }
|
||||
end
|
||||
|
||||
def authorized_find_all_pending_by_current_user(ids)
|
||||
return Todo.none if ids.blank? || current_user.nil?
|
||||
|
||||
Todo.id_in(ids).for_user(current_user).with_state(todo_state_to_find)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Todos
|
||||
class ResolveMany < BaseMany
|
||||
graphql_name 'TodoResolveMany'
|
||||
|
||||
private
|
||||
|
||||
def process_todos(todos)
|
||||
TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done)
|
||||
end
|
||||
|
||||
def todo_state_to_find
|
||||
:pending
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,60 +2,18 @@
|
|||
|
||||
module Mutations
|
||||
module Todos
|
||||
class RestoreMany < ::Mutations::BaseMutation
|
||||
class RestoreMany < BaseMany
|
||||
graphql_name 'TodoRestoreMany'
|
||||
|
||||
MAX_UPDATE_AMOUNT = 100
|
||||
|
||||
argument :ids,
|
||||
[::Types::GlobalIDType[::Todo]],
|
||||
required: true,
|
||||
description: 'Global IDs of the to-do items to restore (a maximum of 100 is supported at once).'
|
||||
|
||||
field :todos, [::Types::TodoType],
|
||||
null: false,
|
||||
description: 'Updated to-do items.'
|
||||
|
||||
def resolve(ids:)
|
||||
check_update_amount_limit!(ids)
|
||||
|
||||
todos = authorized_find_all_pending_by_current_user(model_ids_of(ids))
|
||||
updated_ids = restore(todos)
|
||||
|
||||
{
|
||||
updated_ids: updated_ids,
|
||||
todos: Todo.id_in(updated_ids),
|
||||
errors: errors_on_objects(todos)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def model_ids_of(ids)
|
||||
ids.filter_map { |gid| gid.model_id.to_i }
|
||||
end
|
||||
|
||||
def raise_too_many_todos_requested_error
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, 'Too many to-do items requested.'
|
||||
end
|
||||
|
||||
def check_update_amount_limit!(ids)
|
||||
raise_too_many_todos_requested_error if ids.size > MAX_UPDATE_AMOUNT
|
||||
end
|
||||
|
||||
def errors_on_objects(todos)
|
||||
todos.flat_map { |todo| errors_on_object(todo) }
|
||||
end
|
||||
|
||||
def authorized_find_all_pending_by_current_user(ids)
|
||||
return Todo.none if ids.blank? || current_user.nil?
|
||||
|
||||
Todo.id_in(ids).for_user(current_user).done
|
||||
end
|
||||
|
||||
def restore(todos)
|
||||
def process_todos(todos)
|
||||
TodoService.new.restore_todos(todos, current_user)
|
||||
end
|
||||
|
||||
def todo_state_to_find
|
||||
:done
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,16 +28,24 @@ module Resolvers
|
|||
def resolve_with_lookahead(**args)
|
||||
projects = StarredProjectsFinder.new(
|
||||
user,
|
||||
params: {
|
||||
search: args[:search],
|
||||
sort: args[:sort],
|
||||
min_access_level: args[:min_access_level],
|
||||
language_name: args[:programming_language_name]
|
||||
},
|
||||
params: finder_params(args),
|
||||
current_user: current_user
|
||||
).execute
|
||||
|
||||
apply_lookahead(projects)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def finder_params(args)
|
||||
{
|
||||
search: args[:search],
|
||||
sort: args[:sort],
|
||||
min_access_level: args[:min_access_level],
|
||||
language_name: args[:programming_language_name]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Resolvers::UserStarredProjectsResolver.prepend_mod
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
class NamespaceSettingsType < BaseObject
|
||||
graphql_name 'CiCdSettings'
|
||||
|
||||
authorize :maintainer_access
|
||||
|
||||
field :pipeline_variables_default_role, GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'Indicates the default minimum role required to override pipeline variables in the namespace.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
class PipelineVariablesDefaultRoleTypeEnum < BaseEnum
|
||||
graphql_name 'PipelineVariablesDefaultRoleType'
|
||||
description 'Pipeline variables minimum override roles.'
|
||||
|
||||
ProjectCiCdSetting::PIPELINE_VARIABLES_OVERRIDE_ROLES.keys.map(&:to_s).each do |role|
|
||||
value role.upcase, value: role, description: role.humanize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -145,6 +145,7 @@ module Types
|
|||
mount_mutation Mutations::Todos::MarkDone
|
||||
mount_mutation Mutations::Todos::Restore
|
||||
mount_mutation Mutations::Todos::MarkAllDone
|
||||
mount_mutation Mutations::Todos::ResolveMany, experiment: { milestone: '17.9' }
|
||||
mount_mutation Mutations::Todos::RestoreMany
|
||||
mount_mutation Mutations::Todos::Snooze, experiment: { milestone: '17.4' }
|
||||
mount_mutation Mutations::Todos::UnSnooze, experiment: { milestone: '17.4' }
|
||||
|
|
@ -183,6 +184,7 @@ module Types
|
|||
mount_mutation Mutations::Ci::JobTokenScope::UpdateJobTokenPolicies, experiment: { milestone: '17.6' }
|
||||
mount_mutation Mutations::Ci::JobTokenScope::AutopopulateAllowlist, experiment: { milestone: '17.9' }
|
||||
mount_mutation Mutations::Ci::JobTokenScope::ClearAllowlistAutopopulations, experiment: { milestone: '17.9' }
|
||||
mount_mutation Mutations::Ci::NamespaceSettingsUpdate, experiment: { milestone: '17.9' }
|
||||
mount_mutation Mutations::Ci::Pipeline::Cancel
|
||||
mount_mutation Mutations::Ci::Pipeline::Create
|
||||
mount_mutation Mutations::Ci::Pipeline::Destroy
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ module Types
|
|||
null: true,
|
||||
description: 'Package settings for the namespace.'
|
||||
|
||||
field :ci_cd_settings,
|
||||
Types::Ci::NamespaceSettingsType,
|
||||
null: true,
|
||||
experiment: { milestone: '17.9' },
|
||||
description: 'Namespace CI/CD settings for the namespace.',
|
||||
method: :namespace_settings
|
||||
|
||||
field :shared_runners_setting,
|
||||
Types::Namespace::SharedRunnersSettingEnum,
|
||||
null: true,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ class NamespaceSetting < ApplicationRecord
|
|||
|
||||
self.primary_key = :namespace_id
|
||||
|
||||
def self.declarative_policy_class
|
||||
"Ci::NamespaceSettingPolicy"
|
||||
end
|
||||
|
||||
def self.allowed_namespace_settings_params
|
||||
NAMESPACE_SETTINGS_PARAMS
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class NamespaceSettingPolicy < BasePolicy
|
||||
delegate { @subject.namespace }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
module NamespaceSettings
|
||||
class UpdateService
|
||||
def initialize(settings, args)
|
||||
@settings = settings
|
||||
@args = args
|
||||
end
|
||||
|
||||
def execute
|
||||
return ServiceResponse.success if settings.update(args)
|
||||
|
||||
ServiceResponse.error(message: settings.errors.full_messages)
|
||||
rescue ArgumentError => e
|
||||
ServiceResponse.error(message: [e.message])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :args, :settings
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,10 @@ module ContainerRegistry
|
|||
return service_response_error(message: _('Maximum number of protection rules have been reached.'))
|
||||
end
|
||||
|
||||
unless ::ContainerRegistry::GitlabApiClient.supports_gitlab_api?
|
||||
return service_response_error(message: _('GitLab container registry API not supported'))
|
||||
end
|
||||
|
||||
protection_rule =
|
||||
project.container_registry_protection_tag_rules.create(params.slice(*ALLOWED_ATTRIBUTES))
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ module ContainerRegistry
|
|||
|
||||
def execute
|
||||
unless can?(current_user, :admin_container_image, container_protection_tag_rule.project)
|
||||
error_message = _('Unauthorized to delete a protection rule for container image tags')
|
||||
return service_response_error(message: error_message)
|
||||
return service_response_error(message: _('Unauthorized to delete a protection rule for container image tags'))
|
||||
end
|
||||
|
||||
unless ::ContainerRegistry::GitlabApiClient.supports_gitlab_api?
|
||||
return service_response_error(message: _('GitLab container registry API not supported'))
|
||||
end
|
||||
|
||||
deleted_container_protection_tag_rule = container_protection_tag_rule.destroy!
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ module ContainerRegistry
|
|||
|
||||
def execute
|
||||
unless can?(current_user, :admin_container_image, container_protection_tag_rule.project)
|
||||
error_message = _('Unauthorized to update a protection rule for container image tags')
|
||||
return service_response_error(message: error_message)
|
||||
return service_response_error(message: _('Unauthorized to update a protection rule for container image tags'))
|
||||
end
|
||||
|
||||
unless ::ContainerRegistry::GitlabApiClient.supports_gitlab_api?
|
||||
return service_response_error(message: _('GitLab container registry API not supported'))
|
||||
end
|
||||
|
||||
unless container_protection_tag_rule.update(params.slice(*ALLOWED_ATTRIBUTES))
|
||||
|
|
|
|||
|
|
@ -1431,7 +1431,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="queryvulnerabilitieshasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. |
|
||||
| <a id="queryvulnerabilitieshasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have remediations. |
|
||||
| <a id="queryvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="queryvulnerabilitiesidentifiername"></a>`identifierName` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.6. **Status**: Experiment. Filter vulnerabilities by identifier name. Applicable on project level when feature flag `vulnerability_filtering_by_identifier` is enabled. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance securitydashboard queries. |
|
||||
| <a id="queryvulnerabilitiesidentifiername"></a>`identifierName` | [`String`](#string) | Filter vulnerabilities by identifier name. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance security dashboard queries. |
|
||||
| <a id="queryvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
| <a id="queryvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 category. Wildcard value "NONE" also supported and it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="queryvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
|
|
@ -8065,6 +8065,30 @@ Input type: `NamespaceDeleteRemoteDevelopmentClusterAgentMappingInput`
|
|||
| <a id="mutationnamespacedeleteremotedevelopmentclusteragentmappingclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacedeleteremotedevelopmentclusteragentmappingerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.namespaceSettingsUpdate`
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.9.
|
||||
**Status**: Experiment.
|
||||
|
||||
Input type: `NamespaceSettingsUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationnamespacesettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacesettingsupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace the settings belong to. |
|
||||
| <a id="mutationnamespacesettingsupdatepipelinevariablesdefaultrole"></a>`pipelineVariablesDefaultRole` | [`PipelineVariablesDefaultRoleType`](#pipelinevariablesdefaultroletype) | Indicates the default minimum role required to override pipeline variables in the namespace. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationnamespacesettingsupdatecicdsettings"></a>`ciCdSettings` | [`CiCdSettings!`](#cicdsettings) | Namespace CI/CD settings after mutation. |
|
||||
| <a id="mutationnamespacesettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacesettingsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.noteConvertToThread`
|
||||
|
||||
Convert a standard comment to a resolvable thread.
|
||||
|
|
@ -10308,6 +10332,29 @@ Input type: `TodoMarkDoneInput`
|
|||
| <a id="mutationtodomarkdoneerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationtodomarkdonetodo"></a>`todo` | [`Todo!`](#todo) | Requested to-do item. |
|
||||
|
||||
### `Mutation.todoResolveMany`
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.9.
|
||||
**Status**: Experiment.
|
||||
|
||||
Input type: `TodoResolveManyInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtodoresolvemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtodoresolvemanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to process (a maximum of 100 is supported at once). |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtodoresolvemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtodoresolvemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationtodoresolvemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. |
|
||||
|
||||
### `Mutation.todoRestore`
|
||||
|
||||
Input type: `TodoRestoreInput`
|
||||
|
|
@ -10336,7 +10383,7 @@ Input type: `TodoRestoreManyInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationtodorestoremanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationtodorestoremanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to restore (a maximum of 100 is supported at once). |
|
||||
| <a id="mutationtodorestoremanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to process (a maximum of 100 is supported at once). |
|
||||
|
||||
#### Fields
|
||||
|
||||
|
|
@ -20887,6 +20934,14 @@ Represents a component usage in a project.
|
|||
| <a id="cicatalogresourceversionreadmehtml"></a>`readmeHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `readme`. |
|
||||
| <a id="cicatalogresourceversionreleasedat"></a>`releasedAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in GitLab 16.7. **Status**: Experiment. Timestamp of when the version was released. |
|
||||
|
||||
### `CiCdSettings`
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="cicdsettingspipelinevariablesdefaultrole"></a>`pipelineVariablesDefaultRole` | [`String`](#string) | Indicates the default minimum role required to override pipeline variables in the namespace. |
|
||||
|
||||
### `CiConfig`
|
||||
|
||||
#### Fields
|
||||
|
|
@ -25487,6 +25542,7 @@ GPG signature for a signed commit.
|
|||
| <a id="groupamazons3configurations"></a>`amazonS3Configurations` | [`AmazonS3ConfigurationTypeConnection`](#amazons3configurationtypeconnection) | Amazon S3 configurations that receive audit events belonging to the group. (see [Connections](#connections)) |
|
||||
| <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
|
||||
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
|
||||
| <a id="groupcicdsettings"></a>`ciCdSettings` **{warning-solid}** | [`CiCdSettings`](#cicdsettings) | **Introduced** in GitLab 17.9. **Status**: Experiment. Namespace CI/CD settings for the namespace. |
|
||||
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
|
||||
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
|
||||
| <a id="groupcreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of the group creation. |
|
||||
|
|
@ -26895,7 +26951,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupvulnerabilitieshasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. |
|
||||
| <a id="groupvulnerabilitieshasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have remediations. |
|
||||
| <a id="groupvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="groupvulnerabilitiesidentifiername"></a>`identifierName` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.6. **Status**: Experiment. Filter vulnerabilities by identifier name. Applicable on project level when feature flag `vulnerability_filtering_by_identifier` is enabled. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance securitydashboard queries. |
|
||||
| <a id="groupvulnerabilitiesidentifiername"></a>`identifierName` | [`String`](#string) | Filter vulnerabilities by identifier name. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance security dashboard queries. |
|
||||
| <a id="groupvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
| <a id="groupvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 category. Wildcard value "NONE" also supported and it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="groupvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
|
|
@ -30671,6 +30727,7 @@ Product analytics events for a specific month and year.
|
|||
| <a id="namespaceactualsizelimit"></a>`actualSizeLimit` | [`Float`](#float) | The actual storage size limit (in bytes) based on the enforcement type of either repository or namespace. This limit is agnostic of enforcement type. |
|
||||
| <a id="namespaceadditionalpurchasedstoragesize"></a>`additionalPurchasedStorageSize` | [`Float`](#float) | Additional storage purchased for the root namespace in bytes. |
|
||||
| <a id="namespaceallowedcustomstatuses"></a>`allowedCustomStatuses` **{warning-solid}** | [`WorkItemWidgetCustomStatusConnection`](#workitemwidgetcustomstatusconnection) | **Introduced** in GitLab 17.8. **Status**: Experiment. Allowed custom statuses for the namespace. |
|
||||
| <a id="namespacecicdsettings"></a>`ciCdSettings` **{warning-solid}** | [`CiCdSettings`](#cicdsettings) | **Introduced** in GitLab 17.9. **Status**: Experiment. Namespace CI/CD settings for the namespace. |
|
||||
| <a id="namespacecontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
|
||||
| <a id="namespacecrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
|
||||
| <a id="namespacedescription"></a>`description` | [`String`](#string) | Description of the namespace. |
|
||||
|
|
@ -34433,7 +34490,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="projectvulnerabilitieshasmergerequest"></a>`hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. |
|
||||
| <a id="projectvulnerabilitieshasremediations"></a>`hasRemediations` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have remediations. |
|
||||
| <a id="projectvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
|
||||
| <a id="projectvulnerabilitiesidentifiername"></a>`identifierName` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.6. **Status**: Experiment. Filter vulnerabilities by identifier name. Applicable on project level when feature flag `vulnerability_filtering_by_identifier` is enabled. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance securitydashboard queries. |
|
||||
| <a id="projectvulnerabilitiesidentifiername"></a>`identifierName` | [`String`](#string) | Filter vulnerabilities by identifier name. Applicable on group level when feature flag `vulnerability_filtering_by_identifier_group` is enabled. Ignored when applied on instance security dashboard queries. |
|
||||
| <a id="projectvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
|
||||
| <a id="projectvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 category. Wildcard value "NONE" also supported and it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="projectvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
|
|
@ -34463,11 +34520,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
##### `Project.vulnerabilityIdentifierSearch`
|
||||
|
||||
Search for vulnerabilities by identifier. Feature flag `vulnerability_filtering_by_identifier` has to be enabled for the project.
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.7.
|
||||
**Status**: Experiment.
|
||||
Search for vulnerabilities by identifier.
|
||||
|
||||
Returns [`[String!]`](#string).
|
||||
|
||||
|
|
@ -39086,14 +39139,14 @@ Represents a workspaces agent config.
|
|||
| <a id="workspacesagentconfigannotations"></a>`annotations` | [`[KubernetesAnnotation!]!`](#kubernetesannotation) | Annotations to apply to Kubernetes objects. |
|
||||
| <a id="workspacesagentconfigclusteragent"></a>`clusterAgent` | [`ClusterAgent!`](#clusteragent) | Cluster agent that the workspaces agent config belongs to. |
|
||||
| <a id="workspacesagentconfigcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the workspaces agent config was created. |
|
||||
| <a id="workspacesagentconfigdefaultmaxhoursbeforetermination"></a>`defaultMaxHoursBeforeTermination` | [`Int!`](#int) | Default max hours before worksapce termination of the workspaces agent config. |
|
||||
| <a id="workspacesagentconfigdefaultmaxhoursbeforetermination"></a>`defaultMaxHoursBeforeTermination` **{warning-solid}** | [`Int!`](#int) | **Deprecated** in GitLab 17.9. Field is not used. |
|
||||
| <a id="workspacesagentconfigdefaultruntimeclass"></a>`defaultRuntimeClass` | [`String!`](#string) | Default Kubernetes RuntimeClass. |
|
||||
| <a id="workspacesagentconfigdnszone"></a>`dnsZone` | [`String!`](#string) | DNS zone where workspaces are available. |
|
||||
| <a id="workspacesagentconfigenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether remote development is enabled for the GitLab agent. |
|
||||
| <a id="workspacesagentconfiggitlabworkspacesproxynamespace"></a>`gitlabWorkspacesProxyNamespace` | [`String!`](#string) | Namespace where gitlab-workspaces-proxy is installed. |
|
||||
| <a id="workspacesagentconfigid"></a>`id` | [`RemoteDevelopmentWorkspacesAgentConfigID!`](#remotedevelopmentworkspacesagentconfigid) | Global ID of the workspaces agent config. |
|
||||
| <a id="workspacesagentconfiglabels"></a>`labels` | [`[KubernetesLabel!]!`](#kuberneteslabel) | Labels to apply to Kubernetes objects. |
|
||||
| <a id="workspacesagentconfigmaxhoursbeforeterminationlimit"></a>`maxHoursBeforeTerminationLimit` | [`Int!`](#int) | Max hours before worksapce termination limit of the workspaces agent config. |
|
||||
| <a id="workspacesagentconfigmaxhoursbeforeterminationlimit"></a>`maxHoursBeforeTerminationLimit` **{warning-solid}** | [`Int!`](#int) | **Deprecated** in GitLab 17.9. Field is not used. |
|
||||
| <a id="workspacesagentconfignetworkpolicyenabled"></a>`networkPolicyEnabled` | [`Boolean!`](#boolean) | Whether the network policy of the workspaces agent config is enabled. |
|
||||
| <a id="workspacesagentconfigprojectid"></a>`projectId` | [`ID`](#id) | ID of the project that the workspaces agent config belongs to. |
|
||||
| <a id="workspacesagentconfigupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable workspaces agent config property. |
|
||||
|
|
@ -41737,6 +41790,17 @@ Pipeline security report finding sort values.
|
|||
| <a id="pipelinestatusenumwaiting_for_callback"></a>`WAITING_FOR_CALLBACK` | Pipeline is waiting for an external action. |
|
||||
| <a id="pipelinestatusenumwaiting_for_resource"></a>`WAITING_FOR_RESOURCE` | A resource (for example, a runner) that the pipeline requires to run is unavailable. |
|
||||
|
||||
### `PipelineVariablesDefaultRoleType`
|
||||
|
||||
Pipeline variables minimum override roles.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="pipelinevariablesdefaultroletypedeveloper"></a>`DEVELOPER` | Developer. |
|
||||
| <a id="pipelinevariablesdefaultroletypemaintainer"></a>`MAINTAINER` | Maintainer. |
|
||||
| <a id="pipelinevariablesdefaultroletypeno_one_allowed"></a>`NO_ONE_ALLOWED` | No one allowed. |
|
||||
| <a id="pipelinevariablesdefaultroletypeowner"></a>`OWNER` | Owner. |
|
||||
|
||||
### `PolicyProjectCreatedStatus`
|
||||
|
||||
Types of security policy project created status.
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ DETAILS:
|
|||
|
||||
Use this API to interact with project access tokens. For more information, see [Project access tokens](../user/project/settings/project_access_tokens.md).
|
||||
|
||||
## List project access tokens
|
||||
## List all project access tokens
|
||||
|
||||
> - `state` attribute [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/462217) in GitLab 17.2.
|
||||
|
||||
Get a list of [project access tokens](../user/project/settings/project_access_tokens.md).
|
||||
Lists all project access tokens for a specified project.
|
||||
|
||||
In GitLab 17.2 and later, you can use the `state` attribute to limit the response to project access tokens with a specified state.
|
||||
|
||||
|
|
@ -25,13 +25,15 @@ GET projects/:id/access_tokens
|
|||
GET projects/:id/access_tokens?state=inactive
|
||||
```
|
||||
|
||||
| Attribute | Type | required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-paths) |
|
||||
| `state` | string | No | Limit results to tokens with specified state. Valid values are `active` and `inactive`. By default both states are returned. |
|
||||
| Attribute | Type | required | Description |
|
||||
| --------- | ----------------- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-paths) of a project. |
|
||||
| `state` | string | No | If defined, only returns tokens with the specified state. Possible values: `active` and `inactive`. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
|
||||
curl --request GET \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
|
||||
```
|
||||
|
||||
```json
|
||||
|
|
@ -67,21 +69,23 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
]
|
||||
```
|
||||
|
||||
## Get a project access token
|
||||
## Get details on a project access token
|
||||
|
||||
Get a [project access token](../user/project/settings/project_access_tokens.md) by ID.
|
||||
Gets details on a project access token. You can either reference a specific project access token, or use the keyword `self` to return details on the authenticating project access token.
|
||||
|
||||
```plaintext
|
||||
GET projects/:id/access_tokens/:token_id
|
||||
```
|
||||
|
||||
| Attribute | Type | required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-paths) |
|
||||
| `token_id` | integer | yes | ID of the project access token |
|
||||
| Attribute | Type | required | Description |
|
||||
| ---------- | ----------------- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-paths) of a project. |
|
||||
| `token_id` | integer or string | yes | ID of a project access token or the keyword `self`. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>"
|
||||
curl --request GET \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>"
|
||||
```
|
||||
|
||||
```json
|
||||
|
|
@ -105,31 +109,26 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
|
||||
> - The `expires_at` attribute default was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120213) in GitLab 16.0.
|
||||
|
||||
Create a [project access token](../user/project/settings/project_access_tokens.md).
|
||||
|
||||
When you create a project access token, the maximum role (access level) you set depends on if you have the Owner or Maintainer role for the group. For example, the maximum
|
||||
role that can be set is:
|
||||
|
||||
- Owner (`50`), if you have the Owner role for the project.
|
||||
- Maintainer (`40`), if you have the Maintainer role on the project.
|
||||
Creates a project access token for a specified project. You cannot create a token with an access level greater than your account. For example, a user with the Maintainer role cannot create a project access token with the Owner role.
|
||||
|
||||
```plaintext
|
||||
POST projects/:id/access_tokens
|
||||
```
|
||||
|
||||
| Attribute | Type | required | Description |
|
||||
|-----------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-paths) |
|
||||
| `name` | string | yes | Name of the project access token |
|
||||
| `scopes` | `Array[String]` | yes | [List of scopes](../user/project/settings/project_access_tokens.md#scopes-for-a-project-access-token) |
|
||||
| `access_level` | integer | no | Access level. Valid values are `10` (Guest), `15` (Planner), `20` (Reporter), `30` (Developer), `40` (Maintainer), and `50` (Owner). Defaults to `40`. |
|
||||
| `expires_at` | date | yes | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If undefined, the date is set to the [maximum allowable lifetime limit](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
| Attribute | Type | required | Description |
|
||||
| -------------- | ----------------- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-paths) of a project. |
|
||||
| `name` | string | yes | Name of the token. |
|
||||
| `scopes` | `Array[String]` | yes | List of [scopes](../user/project/settings/project_access_tokens.md#scopes-for-a-project-access-token) available to the token. |
|
||||
| `access_level` | integer | no | [Access level](../development/permissions/predefined_roles.md#members) for the token. Possible values: `10` (Guest), `15` (Planner), `20` (Reporter), `30` (Developer), `40` (Maintainer), and `50` (Owner). Default value: `40`. |
|
||||
| `expires_at` | date | yes | Expiration date of the token in ISO format (`YYYY-MM-DD`). If undefined, the date is set to the [maximum allowable lifetime limit](../user/profile/personal_access_tokens.md#access-token-expiration). |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type:application/json" \
|
||||
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31", "access_level":30 }' \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type:application/json" \
|
||||
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31", "access_level":30 }' \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
|
||||
```
|
||||
|
||||
```json
|
||||
|
|
@ -152,36 +151,31 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
|
||||
## Rotate a project access token
|
||||
|
||||
Rotate a project access token. Revokes the previous token and creates a new token that expires in one week.
|
||||
|
||||
You can either:
|
||||
|
||||
- Use a project access token ID.
|
||||
- In GitLab 17.9 and later, pass the project access token to the API in a request header.
|
||||
|
||||
### Use a project access token ID
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0
|
||||
> - `expires_at` attribute [added](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6.
|
||||
|
||||
Rotates a project access token. This immediately revokes the previous token and creates a new token. Generally, this endpoint rotates a specific project access token by authenticating with a personal access token. You can also use a project access token to rotate itself. For more information, see [Self-rotation](#self-rotation).
|
||||
|
||||
If you attempt to use the revoked token later, GitLab immediately revokes the new token. For more information, see [Automatic reuse detection](personal_access_tokens.md#automatic-reuse-detection).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have a [personal access token with the `api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
|
||||
|
||||
In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date can be up to a maximum of one year from the rotation date.
|
||||
- A personal access token with the [`api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes) or a project access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). See [Self-rotation](#self-rotation).
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/access_tokens/:token_id/rotate
|
||||
```
|
||||
|
||||
| Attribute | Type | required | Description |
|
||||
|-----------|------------|----------|---------------------|
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-paths) |
|
||||
| `token_id` | integer | yes | ID of the project access token |
|
||||
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. If undefined, the token expires after one week. |
|
||||
| Attribute | Type | required | Description |
|
||||
| ------------ | ----------------- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-paths) of a project. |
|
||||
| `token_id` | integer or string | yes | ID of a project access token or the keyword `self`. |
|
||||
| `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). The date must be one year or less from the rotation date. If undefined, the token expires after one week. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>/rotate"
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>/rotate"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
|
@ -203,92 +197,58 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
#### Responses
|
||||
If successful, returns `200: OK`.
|
||||
|
||||
- `200: OK` if the existing token is successfully revoked and the new token is successfully created.
|
||||
- `400: Bad Request` if not rotated successfully.
|
||||
- `401: Unauthorized` if either the:
|
||||
- User does not have access to the token with the specified ID.
|
||||
- Token with the specified ID does not exist.
|
||||
- `401: Unauthorized` if any of the following conditions are true:
|
||||
- You do not have access to the specified token.
|
||||
- The specified token does not exist.
|
||||
- You're authenticating with a project access token. Use [`/projects/:id/access_tokens/self/rotate`](#use-a-request-header). instead.
|
||||
- `404: Not Found` if the user is an administrator but the token with the specified ID does not exist.
|
||||
Other possible responses:
|
||||
|
||||
### Use a request header
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9
|
||||
|
||||
Requires:
|
||||
|
||||
- `api` or `self_rotate` scope.
|
||||
|
||||
In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration).
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/access_tokens/self/rotate
|
||||
```
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_project_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/self/rotate"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"name": "Rotated Token",
|
||||
"revoked": false,
|
||||
"created_at": "2025-01-19T15:00:00.000Z",
|
||||
"description": "Test project access token",
|
||||
"scopes": ["read_api","self_rotate"],
|
||||
"user_id": 1337,
|
||||
"last_used_at": null,
|
||||
"active": true,
|
||||
"expires_at": "2025-01-26",
|
||||
"access_level": 30,
|
||||
"token": "s3cr3t"
|
||||
}
|
||||
```
|
||||
|
||||
#### Responses
|
||||
|
||||
- `200: OK` if the existing project access token is successfully revoked and the new token successfully created.
|
||||
- `400: Bad Request` if not rotated successfully.
|
||||
- `401: Unauthorized` if any of the following conditions are true:
|
||||
- The token does not exist.
|
||||
- The token has expired.
|
||||
- The token was revoked.
|
||||
- The token is not a project access token associated with the specified project.
|
||||
- You do not have access to the specified token.
|
||||
- You're using a project access token to rotate another project access token. See [Self-rotate a project access token](#self-rotation) instead.
|
||||
- `403: Forbidden` if the token is not allowed to rotate itself.
|
||||
- `404: Not Found` if the user is an administrator but the token with the specified ID does not exist.
|
||||
- `405: Method Not Allowed` if the token is not a project access token.
|
||||
|
||||
### Automatic reuse detection
|
||||
### Self-rotation
|
||||
|
||||
Refer to [automatic reuse detection for personal access tokens](personal_access_tokens.md#automatic-reuse-detection)
|
||||
for more information.
|
||||
Instead of rotating a specific project access token, you can instead rotate the same project access token you used to authenticate the request. To self-rotate a project access token, you must:
|
||||
|
||||
- Rotate a project access token with the [`api` or `self_rotate` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes).
|
||||
- Use the `self` keyword in the request URL.
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: <your_project_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/self/rotate"
|
||||
```
|
||||
|
||||
## Revoke a project access token
|
||||
|
||||
Revoke a [project access token](../user/project/settings/project_access_tokens.md).
|
||||
Revokes a specified project access token.
|
||||
|
||||
```plaintext
|
||||
DELETE projects/:id/access_tokens/:token_id
|
||||
```
|
||||
|
||||
| Attribute | Type | required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-paths) |
|
||||
| `token_id` | integer | yes | ID of the project access token |
|
||||
| Attribute | Type | required | Description |
|
||||
| ---------- | ----------------- | -------- | ----------- |
|
||||
| `id` | integer or string | yes | ID or [URL-encoded path](rest/index.md#namespaced-paths) of a project. |
|
||||
| `token_id` | integer | yes | ID of a project access token. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>"
|
||||
curl --request DELETE \
|
||||
--header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>"
|
||||
```
|
||||
|
||||
### Responses
|
||||
If successful, returns `204 No content`.
|
||||
|
||||
- `204: No Content` if successfully revoked.
|
||||
- `400 Bad Request` or `404 Not Found` if not revoked successfully.
|
||||
Other possible responses:
|
||||
|
||||
- `400 Bad Request`: Token was not revoked.
|
||||
- `404 Not Found`: Token can not be found.
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ with compute usage or instance runners usage in the current month only.
|
|||
On GitLab.com an in-app banner is displayed and an email notification sent to the
|
||||
namespace owners when the remaining compute minutes is:
|
||||
|
||||
- Less than 30% of the quota.
|
||||
- Less than 25% of the quota.
|
||||
- Less than 5% of the quota.
|
||||
- Completely used (zero minutes remaining).
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ To replace the token:
|
|||
to list tokens that have expired recently. For example, go to `https://gitlab.com/api/v4/personal_access_tokens`,
|
||||
and locate tokens with a specific `expires_at` date.
|
||||
- For project access tokens, use the
|
||||
[project access tokens API](../../api/project_access_tokens.md#list-project-access-tokens)
|
||||
[project access tokens API](../../api/project_access_tokens.md#list-all-project-access-tokens)
|
||||
to list recently expired tokens.
|
||||
- For group access tokens, use the
|
||||
[group access tokens API](../../api/group_access_tokens.md#list-group-access-tokens)
|
||||
|
|
|
|||
|
|
@ -76,10 +76,7 @@ To view the vulnerability report:
|
|||
## Filtering vulnerabilities
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/452492) the **Identifier** filter in GitLab 17.7 [with a flag](../../../administration/feature_flags.md) named `vulnerability_filtering_by_identifier`. Enabled by default.
|
||||
|
||||
FLAG:
|
||||
The availability of the **Identifier** filter is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/502930) in GitLab 17.9. Feature flag `vulnerability_filtering_by_identifier` removed.
|
||||
|
||||
You can filter vulnerabilities in the vulnerability report to more efficiently triage them.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ Use a project access token to authenticate:
|
|||
Project access tokens are similar to [group access tokens](../../group/settings/group_access_tokens.md)
|
||||
and [personal access tokens](../../profile/personal_access_tokens.md), but project access tokens are scoped to a project, so you cannot use them to access resources from other projects.
|
||||
|
||||
In self-managed instances, project access tokens are subject to the same [maximum lifetime limits](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) as personal access tokens if the limit is set.
|
||||
On GitLab Self-Managed instances, project access tokens are subject to the same [maximum lifetime limits](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) as personal access tokens if the limit is set.
|
||||
|
||||
You can use project access tokens:
|
||||
|
||||
- On GitLab SaaS: If you have the Premium or Ultimate license tier, only one project access token is available with a [trial license](https://about.gitlab.com/free-trial/).
|
||||
- On self-managed instances of GitLab: With any license tier. If you have the Free tier,
|
||||
- On GitLab Self-Managed instances: With any license tier. If you have the Free tier,
|
||||
consider [restricting the creation of project access tokens](#restrict-the-creation-of-project-access-tokens) to lower potential abuse.
|
||||
|
||||
You cannot use project access tokens to create other group, project, or personal access tokens.
|
||||
|
|
@ -63,7 +63,7 @@ To create a project access token:
|
|||
- The token expires on that date at midnight UTC. A token with the expiration date of 2024-01-01 expires at 00:00:00 UTC on 2024-01-01.
|
||||
- If you do not enter an expiry date, the expiry date is automatically set to 30 days later than the current date.
|
||||
- By default, this date can be a maximum of 365 days later than the current date. In GitLab 17.6 or later, you can [extend this limit to 400 days](https://gitlab.com/gitlab-org/gitlab/-/issues/461901).
|
||||
- An instance-wide [maximum lifetime](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) setting can limit the maximum allowable lifetime in self-managed instances.
|
||||
- An instance-wide [maximum lifetime](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) setting can limit the maximum allowable lifetime in GitLab Self-Managed instances.
|
||||
1. Select a role for the token.
|
||||
1. Select the [desired scopes](#scopes-for-a-project-access-token).
|
||||
1. Select **Create project access token**.
|
||||
|
|
@ -85,7 +85,7 @@ access tokens on the access tokens page.
|
|||
|
||||
The inactive project access tokens table displays revoked and expired tokens for 30 days after they became inactive.
|
||||
|
||||
Tokens that belong to [an active token family](../../../api/project_access_tokens.md#automatic-reuse-detection) are displayed for 30 days after the latest active token from the family is expired or revoked.
|
||||
Tokens that belong to [an active token family](../../../api/personal_access_tokens.md#automatic-reuse-detection) are displayed for 30 days after the latest active token from the family is expired or revoked.
|
||||
|
||||
### Use the UI
|
||||
|
||||
|
|
|
|||
|
|
@ -9,3 +9,13 @@ Search/NamespacedClass:
|
|||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 25
|
||||
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/lib/active_context/tracker_spec.rb'
|
||||
|
||||
RSpec/VerifiedDoubleReference:
|
||||
Exclude:
|
||||
- 'spec/lib/active_context/bulk_process_queue_spec.rb'
|
||||
- 'spec/lib/active_context/reference_spec.rb'
|
||||
- 'spec/lib/active_context/tracker_spec.rb'
|
||||
|
|
|
|||
|
|
@ -142,6 +142,66 @@ ActiveContext.raw_queues
|
|||
#<Ai::Context::Queues::MergeRequest:0x0000000177cdf370 @shard=1>]
|
||||
```
|
||||
|
||||
### Adding a new reference type
|
||||
|
||||
Create a class under `lib/active_context/references/` and inherit from the `Reference` class and define the following methods:
|
||||
|
||||
Class methods required:
|
||||
|
||||
- `serialize(object, routing)`: defines a string representation of the reference object
|
||||
- `preload_refs` (optional): preload database records to prevent N+1 issues
|
||||
|
||||
Instance methods required:
|
||||
|
||||
- `serialize`: defines a string representation of the reference object
|
||||
- `as_indexed_json`: a hash containing the data representation of the object
|
||||
- `operation`: determines the operation which can be one of `index`, `upsert` or `delete`
|
||||
- `partition_name`: name of the table or index
|
||||
- `identifier`: unique identifier
|
||||
- `routing` (optional)
|
||||
|
||||
Example:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Ai
|
||||
module Context
|
||||
module References
|
||||
class MergeRequest < ::ActiveContext::Reference
|
||||
def self.serialize(record)
|
||||
new(record.id).serialize
|
||||
end
|
||||
|
||||
attr_reader :identifier
|
||||
|
||||
def initialize(identifier)
|
||||
@identifier = identifier.to_i
|
||||
end
|
||||
|
||||
def serialize
|
||||
self.class.join_delimited([identifier].compact)
|
||||
end
|
||||
|
||||
def as_indexed_json
|
||||
{
|
||||
id: identifier
|
||||
}
|
||||
end
|
||||
|
||||
def operation
|
||||
:index
|
||||
end
|
||||
|
||||
def partition_name
|
||||
'ai_context_merge_requests'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Adding a new collection
|
||||
|
||||
A collection maps data to references and specifies a queue to track its references.
|
||||
|
|
@ -151,7 +211,8 @@ To add a new collection:
|
|||
1. Create a new file in the appropriate directory
|
||||
1. Define a class that `includes ActiveContext::Concerns::Collection`
|
||||
1. Implement the `self.queue` class method to return the associated queue
|
||||
1. Implement the `references` instance method to return the references for an object
|
||||
1. Implement the `self.reference_klass` or `self.reference_klasses` class method to return the references for an object
|
||||
1. Implement the `self.routing(object)` class method to determine how an object should be routed
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -166,8 +227,15 @@ module Ai
|
|||
Queues::MergeRequest
|
||||
end
|
||||
|
||||
def references
|
||||
[Search::Elastic::References::Embedding.serialize(object)]
|
||||
def self.reference_klasses
|
||||
[
|
||||
References::Embedding,
|
||||
References::MergeRequest
|
||||
]
|
||||
end
|
||||
|
||||
def self.routing(object)
|
||||
object.project.root_ancestor.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -193,6 +261,10 @@ ActiveContext.track!(MergeRequest.first, collection: Ai::Context::Collections::M
|
|||
ActiveContext.track!(MergeRequest.first, collection: Ai::Context::Collections::MergeRequest, queue: Ai::Context::Queues::Default)
|
||||
```
|
||||
|
||||
```ruby
|
||||
ActiveContext.track!(Ai::Context::References::MergeRequest.new(1), queue: Ai::Context::Queues::MergeRequest)
|
||||
```
|
||||
|
||||
To view all tracked references:
|
||||
|
||||
```ruby
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ module ActiveContext
|
|||
ActiveContext::Queues.raw_queues
|
||||
end
|
||||
|
||||
def self.track!(*objects, collection:, queue: nil)
|
||||
def self.track!(*objects, collection: nil, queue: nil)
|
||||
ActiveContext::Tracker.track!(*objects, collection: collection, queue: queue)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,8 +46,24 @@ module ActiveContext
|
|||
|
||||
return [0, 0] if specs_buffer.blank?
|
||||
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/507973
|
||||
# deserialize refs, preload records, submit docs, flush, etc.
|
||||
refs = deserialize_all(specs_buffer)
|
||||
|
||||
Reference.preload(refs).each do |ref| # rubocop: disable Rails/FindEach -- not ActiveRecord
|
||||
bulk_processor.process(ref)
|
||||
end
|
||||
|
||||
flushing_duration_s = Benchmark.realtime do
|
||||
@failures = bulk_processor.flush
|
||||
end
|
||||
|
||||
logger.info(
|
||||
'class' => self.class.name,
|
||||
'message' => 'bulk_indexer_flushed',
|
||||
'meta.indexing.search_flushing_duration_s' => flushing_duration_s
|
||||
)
|
||||
|
||||
# Re-enqueue any failures so they are retried
|
||||
ActiveContext.track!(@failures, queue: queue)
|
||||
|
||||
# Remove all the successes
|
||||
scores.each do |set_key, (first_score, last_score, count)|
|
||||
|
|
@ -70,6 +86,14 @@ module ActiveContext
|
|||
|
||||
private
|
||||
|
||||
def deserialize_all(specs)
|
||||
specs.filter_map { |spec, _| Reference.deserialize(spec) }
|
||||
end
|
||||
|
||||
def bulk_processor
|
||||
@bulk_processor ||= ActiveContext::BulkProcessor.new
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= ActiveContext::Config.logger
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,20 @@ module ActiveContext
|
|||
def queue
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def routing(_)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def reference_klasses
|
||||
Array.wrap(reference_klass).tap do |klasses|
|
||||
raise NotImplementedError, "#{self} should define reference_klasses or reference_klass" if klasses.empty?
|
||||
end
|
||||
end
|
||||
|
||||
def reference_klass
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :object
|
||||
|
|
@ -22,7 +36,12 @@ module ActiveContext
|
|||
end
|
||||
|
||||
def references
|
||||
raise NotImplementedError
|
||||
reference_klasses = Array.wrap(self.class.reference_klasses)
|
||||
routing = self.class.routing(object)
|
||||
|
||||
reference_klasses.map do |reference_klass|
|
||||
reference_klass.serialize(object, routing)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveContext
|
||||
module Concerns
|
||||
module ReferenceUtils
|
||||
def delimit(string)
|
||||
string.split(self::DELIMITER)
|
||||
end
|
||||
|
||||
def join_delimited(array)
|
||||
[self, array].join(self::DELIMITER)
|
||||
end
|
||||
|
||||
def deserialize_string(string)
|
||||
delimit(string)[1..]
|
||||
end
|
||||
|
||||
def ref_klass(string)
|
||||
klass = delimit(string).first.safe_constantize
|
||||
|
||||
klass if klass && klass < ::ActiveContext::Reference
|
||||
end
|
||||
|
||||
def ref_module
|
||||
to_s.pluralize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -72,11 +72,11 @@ module ActiveContext
|
|||
case ref.operation.to_sym
|
||||
when :index, :upsert
|
||||
[
|
||||
{ update: { _index: ref.index_name, _id: ref.identifier, routing: ref.routing }.compact },
|
||||
{ update: { _index: ref.partition_name, _id: ref.identifier, routing: ref.routing }.compact },
|
||||
{ doc: ref.as_indexed_json, doc_as_upsert: true }
|
||||
]
|
||||
when :delete
|
||||
[{ delete: { _index: ref.index_name, _id: ref.identifier, routing: ref.routing }.compact }]
|
||||
[{ delete: { _index: ref.partition_name, _id: ref.identifier, routing: ref.routing }.compact }]
|
||||
else
|
||||
raise StandardError, "Operation #{ref.operation} is not supported"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveContext
|
||||
class Reference
|
||||
extend Concerns::ReferenceUtils
|
||||
|
||||
DELIMITER = '|'
|
||||
PRELOAD_BATCH_SIZE = 1_000
|
||||
|
||||
class << self
|
||||
def deserialize(string)
|
||||
ref_klass = ref_klass(string)
|
||||
|
||||
if ref_klass
|
||||
ref_klass.instantiate(string)
|
||||
else
|
||||
Search::Elastic::Reference.deserialize(string)
|
||||
end
|
||||
end
|
||||
|
||||
def instantiate(string)
|
||||
new(*deserialize_string(string))
|
||||
end
|
||||
|
||||
def preload(refs)
|
||||
refs.group_by(&:class).each do |klass, class_refs|
|
||||
class_refs.each_slice(PRELOAD_BATCH_SIZE) do |group_slice|
|
||||
klass.preload_refs(group_slice)
|
||||
end
|
||||
end
|
||||
|
||||
refs
|
||||
end
|
||||
|
||||
def serialize
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def preload_refs(refs)
|
||||
refs
|
||||
end
|
||||
|
||||
def klass
|
||||
name.demodulize
|
||||
end
|
||||
end
|
||||
|
||||
def klass
|
||||
self.class.klass
|
||||
end
|
||||
|
||||
def serialize
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def as_indexed_json
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def operation
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def partition_name
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def identifier
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def routing
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
module ActiveContext
|
||||
class Tracker
|
||||
class << self
|
||||
def track!(*objects, collection:, queue: nil)
|
||||
def track!(*objects, collection: nil, queue: nil)
|
||||
references = collect_references(objects.flatten, collection)
|
||||
|
||||
return 0 if references.empty?
|
||||
|
|
@ -19,9 +19,22 @@ module ActiveContext
|
|||
|
||||
def collect_references(objects, collection)
|
||||
objects.flat_map do |obj|
|
||||
collection.new(obj).references
|
||||
if obj.is_a?(ActiveContext::Reference)
|
||||
obj.serialize
|
||||
elsif obj.is_a?(String)
|
||||
obj
|
||||
else
|
||||
next collection.new(obj).references if collection
|
||||
|
||||
logger.warn("ActiveContext unable to track `#{obj}`: Collection must be specified")
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
ActiveContext::Config.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ActiveContext::BulkProcessQueue do
|
||||
let(:queue) { instance_double('ActiveContext::Queue') }
|
||||
let(:shard) { 0 }
|
||||
let(:redis) { instance_double(Redis) }
|
||||
let(:bulk_processor) { instance_double('ActiveContext::BulkProcessor') }
|
||||
let(:logger) { instance_double('Logger', info: nil, error: nil) }
|
||||
|
||||
subject(:bulk_process_queue) { described_class.new(queue, shard) }
|
||||
|
||||
before do
|
||||
allow(ActiveContext::Redis).to receive(:with_redis).and_yield(redis)
|
||||
allow(ActiveContext::BulkProcessor).to receive(:new).and_return(bulk_processor)
|
||||
allow(ActiveContext::Config).to receive(:logger).and_return(logger)
|
||||
allow(bulk_processor).to receive(:process)
|
||||
allow(bulk_processor).to receive(:flush).and_return([])
|
||||
end
|
||||
|
||||
describe '#process' do
|
||||
let(:specs) { [['spec1', 1], ['spec2', 2]] }
|
||||
let(:reference_class) { class_double("ActiveContext::Reference", preload_refs: nil).as_stubbed_const }
|
||||
let(:references) { [instance_double('ActiveContext::Reference'), instance_double('ActiveContext::Reference')] }
|
||||
|
||||
before do
|
||||
allow(queue).to receive(:each_queued_items_by_shard).and_yield(shard, specs)
|
||||
allow(queue).to receive(:redis_set_key).and_return('redis_set_key')
|
||||
allow(queue).to receive(:push)
|
||||
allow(bulk_process_queue).to receive(:deserialize_all).and_return(references)
|
||||
allow(redis).to receive(:zremrangebyscore)
|
||||
allow(references).to receive(:group_by).and_return({ reference_class => references })
|
||||
allow(reference_class).to receive(:preload_refs)
|
||||
allow(ActiveContext::Reference).to receive(:preload).and_return(references)
|
||||
end
|
||||
|
||||
it 'processes specs and flushes the bulk processor' do
|
||||
expect(bulk_processor).to receive(:process).twice
|
||||
expect(bulk_processor).to receive(:flush)
|
||||
|
||||
bulk_process_queue.process(redis)
|
||||
end
|
||||
|
||||
it 'removes processed items from Redis' do
|
||||
expect(redis).to receive(:zremrangebyscore).with('redis_set_key', 1, 2)
|
||||
|
||||
bulk_process_queue.process(redis)
|
||||
end
|
||||
|
||||
it 'returns the count of processed specs and failures' do
|
||||
expect(bulk_process_queue.process(redis)).to eq([2, 0])
|
||||
end
|
||||
|
||||
context 'when there are failures' do
|
||||
let(:failures) { ['failed_spec'] }
|
||||
|
||||
before do
|
||||
allow(bulk_processor).to receive(:flush).and_return(failures)
|
||||
end
|
||||
|
||||
it 're-enqueues failures' do
|
||||
expect(ActiveContext).to receive(:track!).with(failures, queue: queue)
|
||||
|
||||
bulk_process_queue.process(redis)
|
||||
end
|
||||
|
||||
it 'returns the correct count of processed specs and failures' do
|
||||
expect(bulk_process_queue.process(redis)).to eq([2, 1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when specs are empty' do
|
||||
let(:specs) { [] }
|
||||
|
||||
it 'returns [0, 0] without processing' do
|
||||
expect(bulk_processor).not_to receive(:process)
|
||||
expect(bulk_process_queue.process(redis)).to eq([0, 0])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ RSpec.describe ActiveContext::BulkProcessor do
|
|||
operation: :index,
|
||||
id: 1,
|
||||
as_indexed_json: { title: 'Test Issue' },
|
||||
index_name: 'issues',
|
||||
partition_name: 'issues',
|
||||
identifier: '1',
|
||||
routing: 'group_1'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Indexer do
|
|||
operation: :index,
|
||||
id: 1,
|
||||
as_indexed_json: { title: 'Test Issue' },
|
||||
index_name: 'issues',
|
||||
partition_name: 'issues',
|
||||
identifier: '1',
|
||||
routing: 'group_1',
|
||||
serialize: 'issue 1 group_1'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ActiveContext::Reference do
|
||||
describe '.deserialize' do
|
||||
context 'when ref_klass exists' do
|
||||
let(:mock_ref_klass) { class_double("ActiveContext::References::TestReference") }
|
||||
let(:mock_instance) { instance_double("ActiveContext::References::TestReference") }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:ref_klass).and_return(mock_ref_klass)
|
||||
allow(mock_ref_klass).to receive(:new).and_return(mock_instance)
|
||||
end
|
||||
|
||||
it 'instantiates the ref_klass with the string' do
|
||||
expect(mock_ref_klass).to receive(:instantiate).with('test|string')
|
||||
described_class.deserialize('test|string')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ref_klass does not exist' do
|
||||
before do
|
||||
allow(described_class).to receive(:ref_klass).and_return(nil)
|
||||
stub_const('Search::Elastic::Reference', Class.new)
|
||||
end
|
||||
|
||||
it 'falls back to Search::Elastic::Reference.deserialize' do
|
||||
expect(Search::Elastic::Reference).to receive(:deserialize).with('test|string')
|
||||
described_class.deserialize('test|string')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ref_klass' do
|
||||
before do
|
||||
stub_const('ActiveContext::References::TestReference', Class.new(described_class))
|
||||
end
|
||||
|
||||
it 'returns the correct class when it exists' do
|
||||
expect(described_class.ref_klass('ActiveContext::References::TestReference|some|data'))
|
||||
.to eq(ActiveContext::References::TestReference)
|
||||
end
|
||||
|
||||
it 'returns nil when the class does not exist' do
|
||||
expect(described_class.ref_klass('ActiveContext::References::NonExistantReference|some|data')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#klass' do
|
||||
it 'returns the demodulized class name' do
|
||||
expect(described_class.new.klass).to eq('Reference')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ReferenceUtils methods' do
|
||||
describe '.delimit' do
|
||||
it 'splits the string by the delimiter' do
|
||||
expect(described_class.delimit('a|b|c')).to eq(%w[a b c])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.join_delimited' do
|
||||
it 'joins the array with the delimiter' do
|
||||
expect(described_class.join_delimited(%w[a b c])).to eq('ActiveContext::Reference|a|b|c')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ref_module' do
|
||||
it 'returns the pluralized class name' do
|
||||
expect(described_class.ref_module).to eq('ActiveContext::References')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,68 +18,40 @@ RSpec.describe ActiveContext::Tracker do
|
|||
let(:mock_queue) { [] }
|
||||
|
||||
describe '.track!' do
|
||||
context 'with single object' do
|
||||
it 'tracks references and returns count' do
|
||||
result = described_class.track!('test', collection: mock_collection)
|
||||
let(:mock_collection) { double('Collection') }
|
||||
let(:mock_queue) { [] }
|
||||
|
||||
expect(result).to eq(1)
|
||||
expect(mock_collection.queue).to contain_exactly(['ref_test'])
|
||||
end
|
||||
before do
|
||||
allow(mock_collection).to receive(:queue).and_return(mock_queue)
|
||||
end
|
||||
|
||||
context 'with multiple objects' do
|
||||
it 'tracks references for all objects and returns total count' do
|
||||
result = described_class.track!('test1', 'test2', collection: mock_collection)
|
||||
|
||||
expect(result).to eq(2)
|
||||
expect(mock_collection.queue).to contain_exactly(%w[ref_test1 ref_test2])
|
||||
end
|
||||
it 'tracks a string as-is' do
|
||||
expect(described_class.track!('test_string', collection: mock_collection)).to eq(1)
|
||||
expect(mock_queue).to contain_exactly(['test_string'])
|
||||
end
|
||||
|
||||
context 'with nested arrays' do
|
||||
it 'flattens arrays and tracks all references' do
|
||||
result = described_class.track!(['test1', %w[test2 test3]], collection: mock_collection)
|
||||
|
||||
expect(result).to eq(3)
|
||||
expect(mock_collection.queue).to contain_exactly(%w[ref_test1 ref_test2 ref_test3])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with empty input' do
|
||||
it 'returns zero and does not modify queue' do
|
||||
result = described_class.track!([], collection: mock_collection)
|
||||
|
||||
expect(result).to eq(0)
|
||||
expect(mock_collection.queue).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with custom queue' do
|
||||
it 'uses provided queue instead of collection queue' do
|
||||
result = described_class.track!('test', collection: mock_collection, queue: mock_queue)
|
||||
|
||||
expect(result).to eq(1)
|
||||
expect(mock_queue).to contain_exactly(['ref_test'])
|
||||
expect(mock_collection.queue).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when collection does not implement queue method' do
|
||||
let(:invalid_collection) do
|
||||
Class.new do
|
||||
include ActiveContext::Concerns::Collection
|
||||
|
||||
def references
|
||||
["ref"]
|
||||
end
|
||||
it 'serializes ActiveContext::Reference objects' do
|
||||
reference_class = Class.new(ActiveContext::Reference) do
|
||||
def serialize
|
||||
'serialized_reference'
|
||||
end
|
||||
end
|
||||
reference = reference_class.new
|
||||
|
||||
it 'raises NotImplementedError' do
|
||||
expect do
|
||||
described_class.track!('test', collection: invalid_collection)
|
||||
end.to raise_error(NotImplementedError)
|
||||
end
|
||||
expect(described_class.track!(reference, collection: mock_collection)).to eq(1)
|
||||
expect(mock_queue).to contain_exactly(['serialized_reference'])
|
||||
end
|
||||
|
||||
it 'uses collection.references for other objects' do
|
||||
obj = double('SomeObject')
|
||||
collection_instance = instance_double('CollectionInstance')
|
||||
references = [instance_double(ActiveContext::Reference), instance_double(ActiveContext::Reference)]
|
||||
|
||||
allow(mock_collection).to receive(:new).with(obj).and_return(collection_instance)
|
||||
allow(collection_instance).to receive(:references).and_return(references)
|
||||
|
||||
expect(described_class.track!(obj, collection: mock_collection)).to eq(2)
|
||||
expect(mock_queue).to contain_exactly(references)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ code_quality:
|
|||
DOCKER_CERT_PATH: ""
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_TLS_VERIFY: ""
|
||||
CODE_QUALITY_IMAGE_TAG: "0.96.0"
|
||||
CODE_QUALITY_IMAGE_TAG: "0.96.0-gitlab.1"
|
||||
CODE_QUALITY_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/ci-cd/codequality:$CODE_QUALITY_IMAGE_TAG"
|
||||
DOCKER_SOCKET_PATH: /var/run/docker.sock
|
||||
needs: []
|
||||
|
|
|
|||
|
|
@ -403,12 +403,13 @@ module Gitlab
|
|||
response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } }
|
||||
end
|
||||
|
||||
def raw_blame(revision, path, range:)
|
||||
def raw_blame(revision, path, range:, ignore_revisions_blob: nil)
|
||||
request = Gitaly::RawBlameRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
revision: encode_binary(revision),
|
||||
path: encode_binary(path),
|
||||
range: (encode_binary(range) if range)
|
||||
range: (encode_binary(range) if range),
|
||||
ignore_revisions_blob: (encode_binary(ignore_revisions_blob) if ignore_revisions_blob)
|
||||
)
|
||||
|
||||
response = gitaly_client_call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pipelineVariablesDefaultRole": {
|
||||
"type": "ENUM"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
include GraphqlHelpers
|
||||
include JavaScriptFixturesHelpers
|
||||
|
||||
|
|
@ -27,6 +28,10 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
|
|||
update_container_protection_tag_rule_mutation_path =
|
||||
"#{base_path}/mutations/update_container_protection_tag_rule.mutation.graphql"
|
||||
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
context 'when user does not have access to the project' do
|
||||
it "graphql/#{project_container_protection_tag_rules_query_path}.null_project.json" do
|
||||
query = get_graphql_query_as_string(project_container_protection_tag_rules_query_path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Todos::BaseMany, feature_category: :notifications do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
let_it_be(:todo1) { create(:todo, user: user, author: other_user, state: :pending) }
|
||||
let_it_be(:todo2) { create(:todo, user: user, author: other_user, state: :pending) }
|
||||
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: other_user, state: :pending) }
|
||||
|
||||
let(:mutation) { TestTodoManyMutation.new(object: nil, context: query_context, field: nil) }
|
||||
|
||||
before do
|
||||
stub_const('TestTodoManyMutation', Class.new(described_class) do
|
||||
def process_todos(todos)
|
||||
todos.map(&:id)
|
||||
end
|
||||
|
||||
def todo_state_to_find
|
||||
:pending
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
let(:current_user) { user }
|
||||
|
||||
context 'when implemented correctly in subclass' do
|
||||
it 'processes todos and returns the expected structure' do
|
||||
result = mutation.resolve(ids: [global_id_of(todo1), global_id_of(todo2)])
|
||||
|
||||
expect(result).to match(
|
||||
updated_ids: contain_exactly(todo1.id, todo2.id),
|
||||
todos: contain_exactly(todo1, todo2),
|
||||
errors: be_empty
|
||||
)
|
||||
end
|
||||
|
||||
it 'handles empty input gracefully' do
|
||||
result = mutation.resolve(ids: [])
|
||||
|
||||
expect(result).to match(
|
||||
updated_ids: be_empty,
|
||||
todos: be_empty,
|
||||
errors: be_empty
|
||||
)
|
||||
end
|
||||
|
||||
it 'ignores todos belonging to other users' do
|
||||
result = mutation.resolve(ids: [global_id_of(other_user_todo)])
|
||||
|
||||
expect(result).to match(
|
||||
updated_ids: be_empty,
|
||||
todos: be_empty,
|
||||
errors: be_empty
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when abstract methods are not implemented' do
|
||||
it 'raises NotImplementedError for missing process_todos' do
|
||||
stub_const('MissingProcessTodosMutation', Class.new(described_class) do
|
||||
def todo_state_to_find
|
||||
:pending
|
||||
end
|
||||
end)
|
||||
|
||||
mutation = MissingProcessTodosMutation.new(object: nil, context: query_context, field: nil)
|
||||
expect { mutation.resolve(ids: [global_id_of(todo1)]) }
|
||||
.to raise_error(NotImplementedError, /must implement #process_todos/)
|
||||
end
|
||||
|
||||
it 'raises NotImplementedError for missing todo_state_to_find' do
|
||||
stub_const('MissingTodoStateToFindMutation', Class.new(described_class) do
|
||||
def process_todos(todos)
|
||||
todos.map(&:id)
|
||||
end
|
||||
end)
|
||||
|
||||
mutation = MissingTodoStateToFindMutation.new(object: nil, context: query_context, field: nil)
|
||||
expect { mutation.resolve(ids: [global_id_of(todo1)]) }
|
||||
.to raise_error(NotImplementedError, /must implement #todo_state_to_find/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when exceeding maximum update amount' do
|
||||
it 'raises an error when too many todos are requested' do
|
||||
ids = Array.new((described_class::MAX_UPDATE_AMOUNT + 1)) { global_id_of(todo1) }
|
||||
|
||||
expect { mutation.resolve(ids: ids) }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Too many to-do items requested.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#authorized_find_all_pending_by_current_user' do
|
||||
let(:current_user) { user }
|
||||
|
||||
context 'when ids are blank' do
|
||||
it 'returns empty relation' do
|
||||
result = mutation.resolve(ids: [])
|
||||
|
||||
expect(result[:todos]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not authenticated' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it 'returns empty relation' do
|
||||
result = mutation.resolve(ids: [global_id_of(todo1)])
|
||||
|
||||
expect(result[:todos]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Todos::ResolveMany, feature_category: :notifications do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:author) { create(:user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
|
||||
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
|
||||
|
||||
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
|
||||
describe '#process_todos' do
|
||||
it 'resolves a single todo' do
|
||||
result = resolve_mutation([todo1])
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
|
||||
expect(result).to match(
|
||||
errors: be_empty,
|
||||
updated_ids: contain_exactly(todo1.id),
|
||||
todos: contain_exactly(todo1)
|
||||
)
|
||||
end
|
||||
|
||||
it 'handles a todo which is already done as expected' do
|
||||
result = resolve_mutation([todo2])
|
||||
|
||||
expect_states_were_not_changed
|
||||
|
||||
expect(result).to match(
|
||||
errors: be_empty,
|
||||
updated_ids: be_empty,
|
||||
todos: be_empty
|
||||
)
|
||||
end
|
||||
|
||||
it 'ignores requests for todos which do not belong to the current user' do
|
||||
resolve_mutation([other_user_todo])
|
||||
|
||||
expect_states_were_not_changed
|
||||
end
|
||||
|
||||
it 'resolves multiple todos' do
|
||||
todo4 = create(:todo, user: current_user, author: author, state: :pending)
|
||||
|
||||
result = resolve_mutation([todo1, todo4, todo2])
|
||||
|
||||
expect(result[:updated_ids].size).to eq(2)
|
||||
|
||||
returned_todo_ids = result[:updated_ids]
|
||||
expect(returned_todo_ids).to contain_exactly(todo1.id, todo4.id)
|
||||
expect(result[:todos]).to contain_exactly(todo1, todo4)
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(todo4.reload.state).to eq('done')
|
||||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
end
|
||||
|
||||
it 'fails if one todo does not belong to the current user' do
|
||||
resolve_mutation([todo1, todo2, other_user_todo])
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
end
|
||||
|
||||
it 'fails if too many todos are requested for update' do
|
||||
expect { resolve_mutation([todo1] * 101) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_mutation(todos)
|
||||
mutation.resolve(ids: todos.map { |todo| global_id_of(todo) })
|
||||
end
|
||||
|
||||
def expect_states_were_not_changed
|
||||
expect(todo1.reload.state).to eq('pending')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
end
|
||||
end
|
||||
|
|
@ -16,7 +16,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
|
|||
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
|
||||
describe '#resolve' do
|
||||
describe '#process_todos' do
|
||||
it 'restores a single todo' do
|
||||
result = restore_mutation([todo1])
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::Ci::NamespaceSettingsType, feature_category: :pipeline_composition do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:namespace_settings) { build(:namespace_settings, namespace: namespace) }
|
||||
|
||||
specify { expect(described_class.graphql_name).to eq('CiCdSettings') }
|
||||
|
||||
it 'requires authorization' do
|
||||
expect(described_class).to require_graphql_authorizations(:maintainer_access)
|
||||
end
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expect(described_class).to have_graphql_field(:pipeline_variables_default_role)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['PipelineVariablesDefaultRoleType'], feature_category: :ci_variables do
|
||||
it 'matches the keys of ProjectCiCdSetting.pipeline_variables_minimum_override_role' do
|
||||
expect(described_class.values.keys)
|
||||
.to match_array(%w[NO_ONE_ALLOWED DEVELOPER MAINTAINER OWNER])
|
||||
end
|
||||
end
|
||||
|
|
@ -12,7 +12,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do
|
|||
id name path full_name full_path achievements_path description description_html visibility
|
||||
lfs_enabled request_access_enabled projects root_storage_statistics shared_runners_setting
|
||||
timelog_categories achievements work_item pages_deployments import_source_users work_item_types
|
||||
sidebar work_item_description_templates allowed_custom_statuses
|
||||
sidebar work_item_description_templates allowed_custom_statuses ci_cd_settings
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
|||
|
|
@ -1146,6 +1146,70 @@ RSpec.describe Gitlab::GitalyClient::CommitService, feature_category: :gitaly do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with ignore_revisions_blob' do
|
||||
let(:ignore_revisions_blob) { "refs/heads/#{branch_name}:#{file_name}" }
|
||||
let(:branch_name) { generate :branch }
|
||||
|
||||
subject(:blame) do
|
||||
client.raw_blame(revision, path, range: range, ignore_revisions_blob: ignore_revisions_blob).split("\n")
|
||||
end
|
||||
|
||||
shared_examples 'raises error with message' do |error_class, message|
|
||||
it "raises #{error_class} with correct message" do
|
||||
expect { blame }.to raise_error(error_class) do |error|
|
||||
expect(error.details).to eq(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ignore file exists' do
|
||||
before do
|
||||
project.repository.create_file(
|
||||
project.owner,
|
||||
file_name,
|
||||
file_content,
|
||||
message: "add file",
|
||||
branch_name: branch_name
|
||||
)
|
||||
end
|
||||
|
||||
context "with valid ignore file content" do
|
||||
let(:file_name) { '.git-ignore-revs-file' }
|
||||
let(:file_content) { '3685515c40444faf92774e72835e1f9c0e809672' }
|
||||
|
||||
it 'excludes the specified revision from blame' do
|
||||
expect(blame).to include(*blame_headers[0..2], blame_headers[4])
|
||||
expect(blame).not_to include(file_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid ignore file content' do
|
||||
let(:file_name) { '.git-ignore-revs-invalid' }
|
||||
let(:file_content) { 'invalid_content' }
|
||||
|
||||
include_examples 'raises error with message',
|
||||
GRPC::NotFound,
|
||||
'invalid object name'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid ignore revision' do
|
||||
let(:ignore_revisions_blob) { "refs/heads/invalid" }
|
||||
|
||||
include_examples 'raises error with message',
|
||||
GRPC::NotFound,
|
||||
'cannot resolve ignore-revs blob'
|
||||
end
|
||||
|
||||
context 'when ignore_revision_blob is a directory' do
|
||||
let(:ignore_revisions_blob) { "refs/heads/#{revision}:files" }
|
||||
|
||||
include_examples 'raises error with message',
|
||||
GRPC::InvalidArgument,
|
||||
'ignore revision is not a blob'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a range' do
|
||||
let(:range) { '3,4' }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::NamespaceSettingPolicy, feature_category: :pipeline_composition do
|
||||
subject(:policy) { described_class.new(user, namespace_setting) }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:namespace_setting) { build(:namespace_settings, namespace: namespace) }
|
||||
|
||||
describe 'delegation' do
|
||||
let(:delegations) { policy.delegated_policies }
|
||||
|
||||
it 'delegates to UserNamespacePolicy' do
|
||||
expect(delegations.size).to eq(1)
|
||||
|
||||
delegations.each_value do |delegated_policy|
|
||||
expect(delegated_policy).to be_instance_of(::Namespaces::UserNamespacePolicy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting namespace settings in a namespace', feature_category: :pipeline_composition do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:namespace) { create(:group, :public) }
|
||||
let(:namespace_settings) { namespace.namespace_settings }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
let(:namespace_settings_response) { graphql_data.dig('namespace', 'ciCdSettings') }
|
||||
let(:fields) { all_graphql_fields_for('CiCdSettings') }
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'namespace',
|
||||
{ 'fullPath' => namespace.full_path },
|
||||
query_graphql_field('ci_cd_settings', {}, fields)
|
||||
)
|
||||
end
|
||||
|
||||
let(:execute_query) { post_graphql(query, current_user: current_user) }
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
before do
|
||||
namespace.add_maintainer(current_user)
|
||||
|
||||
execute_query
|
||||
end
|
||||
|
||||
it 'matches the JSON schema' do
|
||||
expect(namespace_settings_response).to match_schema('graphql/ci/namespace_settings')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'NamespaceSettingsUpdate', feature_category: :pipeline_composition do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:namespace) { create(:group, :public) }
|
||||
|
||||
let(:variables) do
|
||||
{
|
||||
full_path: namespace.full_path,
|
||||
pipeline_variables_default_role: 'DEVELOPER'
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:namespace_settings_update, variables, 'errors') }
|
||||
|
||||
subject(:request) { post_graphql_mutation(mutation, current_user: user) }
|
||||
|
||||
context 'when unauthorized' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
shared_examples 'unauthorized' do
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
||||
expect(graphql_errors).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not a namespace member' do
|
||||
it_behaves_like 'unauthorized'
|
||||
end
|
||||
|
||||
context 'when a non-maintainer namespace member' do
|
||||
before_all do
|
||||
namespace.add_developer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'unauthorized'
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'authorized maintainer' do
|
||||
it 'updates pipeline_variables_default_role' do
|
||||
request
|
||||
|
||||
expect(namespace.reload.namespace_settings.pipeline_variables_default_role).to eq('developer')
|
||||
expect(graphql_errors).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authorized' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
context 'with an owner role' do
|
||||
before_all do
|
||||
namespace.add_owner(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'authorized maintainer'
|
||||
|
||||
context 'when update is unsuccessful' do
|
||||
let(:update_service) { instance_double(::Ci::NamespaceSettings::UpdateService) }
|
||||
|
||||
before do
|
||||
allow(::Ci::NamespaceSettings::UpdateService).to receive(:new) { update_service }
|
||||
allow(update_service)
|
||||
.to receive(:execute)
|
||||
.and_return ServiceResponse.error(message: ['Not allowed to update'])
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
||||
expect(graphql_mutation_response(:namespace_settings_update)['errors']).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a maitainer role' do
|
||||
before_all do
|
||||
namespace.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'authorized maintainer'
|
||||
end
|
||||
|
||||
it 'does not update pipeline_variables_default_role if not specified' do
|
||||
variables.except!(:pipeline_variables_default_role)
|
||||
|
||||
request
|
||||
|
||||
expect(namespace.reload.namespace_settings.pipeline_variables_default_role).to eq('no_one_allowed')
|
||||
end
|
||||
|
||||
context 'when bad arguments are provided' do
|
||||
let(:variables) { { full_path: '' } }
|
||||
|
||||
it 'returns the errors' do
|
||||
request
|
||||
|
||||
expect(graphql_errors).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Creating the container registry tag protection rule', :aggregate_failures, feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user, maintainer_of: project) }
|
||||
let_it_be(:current_user) { create(:user, maintainer_of: project) }
|
||||
|
||||
let(:tag_rule_attributes) do
|
||||
build_stubbed(:container_registry_protection_tag_rule, project: project)
|
||||
|
|
@ -36,33 +37,37 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate
|
|||
|
||||
let(:mutation_response) { graphql_mutation_response(:create_container_protection_tag_rule) }
|
||||
|
||||
subject(:post_graphql_mutation_create) do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
subject(:post_graphql_mutation_request) do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end
|
||||
|
||||
shared_examples 'a successful response' do
|
||||
it 'returns the created tag protection rule' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'errors' => be_blank,
|
||||
'containerProtectionTagRule' => {
|
||||
'id' => be_present,
|
||||
'tagNamePattern' => input[:tag_name_pattern]
|
||||
}
|
||||
)
|
||||
post_graphql_mutation_request.tap do
|
||||
expect(mutation_response).to include(
|
||||
'errors' => be_blank,
|
||||
'containerProtectionTagRule' => {
|
||||
'id' => be_present,
|
||||
'tagNamePattern' => input[:tag_name_pattern]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates container registry protection rule in the database' do
|
||||
expect { post_graphql_mutation_create }.to change { ::ContainerRegistry::Protection::TagRule.count }.by(1)
|
||||
expect { post_graphql_mutation_request }.to change { ::ContainerRegistry::Protection::TagRule.count }.by(1)
|
||||
|
||||
expect(::ContainerRegistry::Protection::TagRule.where(project: project,
|
||||
tag_name_pattern: input[:tag_name_pattern])).to exist
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'not changing the protection rule count' do
|
||||
it { expect { post_graphql_mutation_create }.not_to change { ::ContainerRegistry::Protection::TagRule.count } }
|
||||
shared_examples 'not persisting changes' do
|
||||
it { expect { post_graphql_mutation_request }.not_to change { ::ContainerRegistry::Protection::TagRule.count } }
|
||||
end
|
||||
|
||||
it_behaves_like 'a successful response'
|
||||
|
|
@ -75,37 +80,19 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate
|
|||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect_graphql_errors_to_include([/minimumAccessLevelForPush/, /minimumAccessLevelForDelete/])
|
||||
end
|
||||
it_behaves_like 'returning a GraphQL error', [/minimumAccessLevelForPush/, /minimumAccessLevelForDelete/]
|
||||
end
|
||||
|
||||
context 'with blank input for the field `minimumAccessLevelForPush`' do
|
||||
let(:input) { super().merge(minimum_access_level_for_push: nil) }
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect(mutation_response['errors']).to match_array ["Access levels should either both be present or both be nil"]
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
end
|
||||
|
||||
context 'with blank input for the field `minimumAccessLevelForDelete`' do
|
||||
let(:input) { super().merge(minimum_access_level_for_delete: nil) }
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect(mutation_response['errors']).to match_array ["Access levels should either both be present or both be nil"]
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
end
|
||||
|
||||
context 'with both access levels blank' do
|
||||
|
|
@ -117,29 +104,14 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate
|
|||
context 'with blank input field `tagNamePattern`' do
|
||||
let(:input) { super().merge(tag_name_pattern: '') }
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns error from endpoint implementation (not from graphql framework)' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect_graphql_errors_to_include([/tagNamePattern can't be blank/])
|
||||
end
|
||||
it_behaves_like 'returning a GraphQL error', /tagNamePattern can't be blank/
|
||||
end
|
||||
|
||||
context 'with invalid input field `tagNamePattern`' do
|
||||
let(:input) { super().merge(tag_name_pattern: "*") }
|
||||
let(:input) { super().merge(tag_name_pattern: '*') }
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns error from endpoint implementation (not from graphql framework)' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
|
||||
expect(mutation_response['errors']).to eq [
|
||||
"Tag name pattern not valid RE2 syntax: no argument for repetition operator: *"
|
||||
]
|
||||
end
|
||||
it_behaves_like 'returning a mutation error',
|
||||
'Tag name pattern not valid RE2 syntax: no argument for repetition operator: *'
|
||||
end
|
||||
|
||||
context 'with existing containers protection rule' do
|
||||
|
|
@ -154,60 +126,11 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate
|
|||
minimum_access_level_for_push: 'OWNER')
|
||||
end
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns without error' do
|
||||
post_graphql_mutation_create
|
||||
|
||||
expect(mutation_response['errors']).to eq ['Tag name pattern has already been taken']
|
||||
end
|
||||
|
||||
it 'does not create new container protection rules' do
|
||||
expect(::ContainerRegistry::Protection::TagRule.where(project: project,
|
||||
tag_name_pattern: input[:tag_name_pattern],
|
||||
minimum_access_level_for_push: Gitlab::Access::OWNER)).not_to exist
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Tag name pattern has already been taken'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission' do
|
||||
let_it_be(:developer) { create(:user, developer_of: project) }
|
||||
let_it_be(:reporter) { create(:user, reporter_of: project) }
|
||||
let_it_be(:guest) { create(:user, guest_of: project) }
|
||||
let_it_be(:anonymous) { create(:user) }
|
||||
|
||||
where(:user) do
|
||||
[ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation_create.tap do
|
||||
expect_graphql_errors_to_include(/you don't have permission to perform this action/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when feature flag ':container_registry_protected_tags' disabled" do
|
||||
before do
|
||||
stub_feature_flags(container_registry_protected_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not changing the protection rule count'
|
||||
|
||||
it 'does not create a rule' do
|
||||
post_graphql_mutation_create.tap do
|
||||
expect(::ContainerRegistry::Protection::TagRule.where(project: project)).not_to exist
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns error of disabled feature flag' do
|
||||
post_graphql_mutation_create.tap do
|
||||
expect_graphql_errors_to_include(/'container_registry_protected_tags' feature flag is disabled/)
|
||||
end
|
||||
end
|
||||
end
|
||||
include_examples 'when user does not have permission'
|
||||
include_examples 'when feature flag container_registry_protected_tags is disabled'
|
||||
include_examples 'when the GitLab API is not supported'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Deleting a container registry tag protection rule', :aggregate_failures, feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
|
@ -16,31 +17,25 @@ RSpec.describe 'Deleting a container registry tag protection rule', :aggregate_f
|
|||
let(:mutation_response) { graphql_mutation_response(:delete_container_protection_tag_rule) }
|
||||
let(:input) { { id: container_protection_rule.to_global_id } }
|
||||
|
||||
subject(:post_graphql_mutation_delete_container_protection_tag_rule) do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
subject(:post_graphql_mutation_request) do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end
|
||||
|
||||
shared_examples 'an erroneous response' do
|
||||
it { post_graphql_mutation_delete_container_protection_tag_rule.tap { expect(mutation_response).to be_blank } }
|
||||
|
||||
shared_examples 'not persisting changes' do
|
||||
it 'does not delete the protection rule' do
|
||||
expect { post_graphql_mutation_delete_container_protection_tag_rule }
|
||||
expect { post_graphql_mutation_request }
|
||||
.not_to change { ::ContainerRegistry::Protection::TagRule.count }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'returning a permission error' do
|
||||
it 'returns a permission error' do
|
||||
post_graphql_mutation_delete_container_protection_tag_rule
|
||||
|
||||
expect_graphql_errors_to_include(/you don't have permission to perform this action/)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a working GraphQL mutation'
|
||||
|
||||
it 'responds with deleted container registry tag protection rule' do
|
||||
expect { post_graphql_mutation_delete_container_protection_tag_rule }
|
||||
expect { post_graphql_mutation_request }
|
||||
.to change { ::ContainerRegistry::Protection::TagRule.count }.from(1).to(0)
|
||||
|
||||
expect(mutation_response).to include(
|
||||
|
|
@ -57,8 +52,7 @@ RSpec.describe 'Deleting a container registry tag protection rule', :aggregate_f
|
|||
context 'with existing container registry tag protection rule belonging to other project' do
|
||||
let_it_be(:container_protection_rule) { create(:container_registry_protection_tag_rule) }
|
||||
|
||||
it_behaves_like 'an erroneous response'
|
||||
it_behaves_like 'returning a permission error'
|
||||
it_behaves_like 'returning a GraphQL error', /you don't have permission to perform this action/
|
||||
end
|
||||
|
||||
context 'with deleted container registry tag protection rule' do
|
||||
|
|
@ -66,37 +60,10 @@ RSpec.describe 'Deleting a container registry tag protection rule', :aggregate_f
|
|||
create(:container_registry_protection_tag_rule, project: project, tag_name_pattern: 'v1*').destroy!
|
||||
end
|
||||
|
||||
it_behaves_like 'an erroneous response'
|
||||
it_behaves_like 'returning a permission error'
|
||||
it_behaves_like 'returning a GraphQL error', /you don't have permission to perform this action/
|
||||
end
|
||||
|
||||
context 'when current_user does not have permission' do
|
||||
let_it_be(:developer) { create(:user, developer_of: project) }
|
||||
let_it_be(:reporter) { create(:user, reporter_of: project) }
|
||||
let_it_be(:guest) { create(:user, guest_of: project) }
|
||||
let_it_be(:anonymous) { create(:user) }
|
||||
|
||||
where(:current_user) do
|
||||
[ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'an erroneous response'
|
||||
it_behaves_like 'returning a permission error'
|
||||
end
|
||||
end
|
||||
|
||||
context "when feature flag ':container_registry_protected_tags' disabled" do
|
||||
before do
|
||||
stub_feature_flags(container_registry_protected_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'an erroneous response'
|
||||
|
||||
it 'returns an error on the disabled feature flag' do
|
||||
post_graphql_mutation_delete_container_protection_tag_rule
|
||||
|
||||
expect_graphql_errors_to_include(/'container_registry_protected_tags' feature flag is disabled/)
|
||||
end
|
||||
end
|
||||
include_examples 'when user does not have permission'
|
||||
include_examples 'when feature flag container_registry_protected_tags is disabled'
|
||||
include_examples 'when the GitLab API is not supported'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Updating the container registry tag protection rule', :aggregate_failures, feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
|
@ -41,37 +42,42 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate
|
|||
|
||||
let(:mutation_response) { graphql_mutation_response(:update_container_protection_tag_rule) }
|
||||
|
||||
subject(:post_graphql_mutation_update_rule) do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
subject(:post_graphql_mutation_request) do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end
|
||||
|
||||
shared_examples 'a successful response' do
|
||||
it 'returns the updated container registry tag protection rule' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'errors' => be_blank,
|
||||
'containerProtectionTagRule' => {
|
||||
'tagNamePattern' => input[:tag_name_pattern],
|
||||
'minimumAccessLevelForDelete' => input[:minimum_access_level_for_delete],
|
||||
'minimumAccessLevelForPush' => input[:minimum_access_level_for_push]
|
||||
}
|
||||
)
|
||||
post_graphql_mutation_request.tap do
|
||||
expect(mutation_response).to include(
|
||||
'errors' => be_blank,
|
||||
'containerProtectionTagRule' => {
|
||||
'tagNamePattern' => input[:tag_name_pattern],
|
||||
'minimumAccessLevelForDelete' => input[:minimum_access_level_for_delete],
|
||||
'minimumAccessLevelForPush' => input[:minimum_access_level_for_push]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the rule with the right attributes' do
|
||||
post_graphql_mutation_update_rule
|
||||
expect(container_protection_tag_rule.reload).to have_attributes(
|
||||
tag_name_pattern: input[:tag_name_pattern],
|
||||
minimum_access_level_for_push: input[:minimum_access_level_for_push]&.downcase,
|
||||
minimum_access_level_for_delete: input[:minimum_access_level_for_delete]&.downcase
|
||||
)
|
||||
post_graphql_mutation_request.tap do
|
||||
expect(container_protection_tag_rule.reload).to have_attributes(
|
||||
tag_name_pattern: input[:tag_name_pattern],
|
||||
minimum_access_level_for_push: input[:minimum_access_level_for_push]&.downcase,
|
||||
minimum_access_level_for_delete: input[:minimum_access_level_for_delete]&.downcase
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'not updating the tag rule' do
|
||||
shared_examples 'not persisting changes' do
|
||||
it 'does not update the tag rule' do
|
||||
expect { post_graphql_mutation_update_rule }
|
||||
expect { post_graphql_mutation_request }
|
||||
.not_to(change { container_protection_tag_rule.reload.updated_at })
|
||||
end
|
||||
end
|
||||
|
|
@ -88,61 +94,31 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate
|
|||
super().merge(tag_name_pattern: other_existing_container_protection_tag_rule.tag_name_pattern)
|
||||
end
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'does not raise any graphql errors' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
|
||||
it 'returns a blank container registry tag protection rule' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect(mutation_response['containerProtectionTagRule']).to be_blank
|
||||
post_graphql_mutation_request.tap do
|
||||
expect(mutation_response['containerProtectionTagRule']).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes error message in response' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect(mutation_response['errors']).to eq ['Tag name pattern has already been taken']
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Tag name pattern has already been taken'
|
||||
end
|
||||
|
||||
context 'with invalid input param `minimumAccessLevelForPush`' do
|
||||
let(:input) { super().merge(minimum_access_level_for_push: 'INVALID_ACCESS_LEVEL') }
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'raises an invalid value error' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect_graphql_errors_to_include(/invalid value for minimumAccessLevelForPush/)
|
||||
end
|
||||
it_behaves_like 'returning a GraphQL error', /invalid value for minimumAccessLevelForPush/
|
||||
end
|
||||
|
||||
context 'with invalid input param `minimumAccessLevelForDelete`' do
|
||||
let(:input) { super().merge(minimum_access_level_for_delete: 'INVALID_ACCESS_LEVEL') }
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'raises an invalid value error' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect_graphql_errors_to_include(/invalid value for minimumAccessLevelForDelete/)
|
||||
end
|
||||
it_behaves_like 'returning a GraphQL error', /invalid value for minimumAccessLevelForDelete/
|
||||
end
|
||||
|
||||
context 'with invalid input param `tagNamePattern`' do
|
||||
let(:input) { super().merge(tag_name_pattern: '') }
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'returns error with correct error message' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect_graphql_errors_to_include(/tagNamePattern can't be blank/)
|
||||
end
|
||||
it_behaves_like 'returning a GraphQL error', /tagNamePattern can't be blank/
|
||||
end
|
||||
|
||||
context 'with blank input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do
|
||||
|
|
@ -154,60 +130,16 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate
|
|||
context 'with only `minimumAccessLevelForDelete` blank' do
|
||||
let(:input) { super().merge(minimum_access_level_for_delete: nil) }
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'returns error with correct error message' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect(mutation_response['errors']).to match_array ["Access levels should either both be present or both be nil"]
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
end
|
||||
|
||||
context 'with only `minimumAccessLevelForPush` blank' do
|
||||
let(:input) { super().merge(minimum_access_level_for_push: nil) }
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'returns error with correct error message' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect(mutation_response['errors']).to match_array ["Access levels should either both be present or both be nil"]
|
||||
end
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
end
|
||||
|
||||
context 'when current_user does not have permission' do
|
||||
let_it_be(:developer) { create(:user, developer_of: project) }
|
||||
let_it_be(:reporter) { create(:user, reporter_of: project) }
|
||||
let_it_be(:guest) { create(:user, guest_of: project) }
|
||||
let_it_be(:anonymous) { create(:user) }
|
||||
|
||||
where(:current_user) do
|
||||
[ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it 'raises permission errors' do
|
||||
post_graphql_mutation_update_rule
|
||||
expect_graphql_errors_to_include(/you don't have permission to perform this action/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when feature flag ':container_registry_protected_tags' disabled" do
|
||||
before do
|
||||
stub_feature_flags(container_registry_protected_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not updating the tag rule'
|
||||
|
||||
it { post_graphql_mutation_update_rule.tap { expect(mutation_response).to be_blank } }
|
||||
|
||||
it 'returns error of disabled feature flag' do
|
||||
post_graphql_mutation_update_rule
|
||||
|
||||
expect_graphql_errors_to_include(/'container_registry_protected_tags' feature flag is disabled/)
|
||||
end
|
||||
end
|
||||
include_examples 'when user does not have permission'
|
||||
include_examples 'when feature flag container_registry_protected_tags is disabled'
|
||||
include_examples 'when the GitLab API is not supported'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Resolving many Todos', feature_category: :team_planning do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:current_user) { create(:user, developer_of: project) }
|
||||
let_it_be(:author) { create(:user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
let_it_be_with_reload(:todo1) { create(:todo, user: current_user, author: author, state: :pending, target: issue) }
|
||||
let_it_be_with_reload(:todo2) { create(:todo, user: current_user, author: author, state: :pending, target: issue) }
|
||||
|
||||
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
|
||||
|
||||
let(:input_ids) { [todo1, todo2].map { |obj| global_id_of(obj) } }
|
||||
let(:input) { { ids: input_ids } }
|
||||
|
||||
let(:mutation) do
|
||||
graphql_mutation(
|
||||
:todo_resolve_many,
|
||||
input,
|
||||
<<-QL.strip_heredoc
|
||||
clientMutationId
|
||||
errors
|
||||
todos {
|
||||
id
|
||||
state
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
def mutation_response
|
||||
graphql_mutation_response(:todo_resolve_many)
|
||||
end
|
||||
|
||||
it 'resolves many todos' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'errors' => be_empty,
|
||||
'todos' => contain_exactly(
|
||||
a_graphql_entity_for(todo1, 'state' => 'done'),
|
||||
a_graphql_entity_for(todo2, 'state' => 'done')
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when using an invalid gid' do
|
||||
let(:input_ids) { [global_id_of(author)] }
|
||||
let(:invalid_gid_error) { /does not represent an instance of #{todo1.class}/ }
|
||||
|
||||
it 'contains the expected error' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
errors = json_response['errors']
|
||||
expect(errors).not_to be_blank
|
||||
expect(errors.first['message']).to match(invalid_gid_error)
|
||||
|
||||
expect(todo1.reload.state).to eq('pending')
|
||||
expect(todo2.reload.state).to eq('pending')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::NamespaceSettings::UpdateService, feature_category: :pipeline_composition do
|
||||
let(:settings) { instance_double(NamespaceSetting) }
|
||||
let(:args) { { pipeline_variables_default_role: 'developer' } }
|
||||
let(:errors) { instance_double(ActiveModel::Errors, full_messages: ['Error message']) }
|
||||
|
||||
subject(:service) { described_class.new(settings, args) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'when update is successful' do
|
||||
before do
|
||||
allow(settings).to receive(:update).with(args).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns success response' do
|
||||
expect(service.execute).to be_success
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update fails' do
|
||||
before do
|
||||
allow(settings).to receive(:update).with(args).and_return(false)
|
||||
allow(settings).to receive(:errors).and_return(errors)
|
||||
end
|
||||
|
||||
it 'returns error response' do
|
||||
response = service.execute
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq(['Error message'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with real data' do
|
||||
let(:namespace) { create(:group, :public) }
|
||||
let(:settings) { namespace.namespace_settings }
|
||||
|
||||
it 'updates settings successfully' do
|
||||
response = service.execute
|
||||
|
||||
expect(response).to be_success
|
||||
expect(settings.reload.pipeline_variables_default_role).to eq('developer')
|
||||
end
|
||||
|
||||
context 'with invalid data' do
|
||||
let(:args) { { pipeline_variables_default_role: 'invalid_role' } }
|
||||
|
||||
it 'returns error response' do
|
||||
response = service.execute
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute', feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:current_user) { create(:user, maintainer_of: project) }
|
||||
|
||||
|
|
@ -11,6 +13,10 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
|
|||
|
||||
subject(:service_execute) { service.execute }
|
||||
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
shared_examples 'a successful service response' do
|
||||
it_behaves_like 'returning a success service response' do
|
||||
it { is_expected.to have_attributes(errors: be_blank) }
|
||||
|
|
@ -139,4 +145,13 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
|
|||
it_behaves_like 'an erroneous service response',
|
||||
message: 'Maximum number of protection rules have been reached.'
|
||||
end
|
||||
|
||||
context 'when the GitLab API is not supported' do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'an erroneous service response',
|
||||
message: 'GitLab container registry API not supported'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ContainerRegistry::Protection::DeleteTagRuleService, '#execute', feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user, maintainer_of: project) }
|
||||
let_it_be_with_refind(:container_protection_tag_rule) do
|
||||
|
|
@ -13,6 +15,10 @@ RSpec.describe ContainerRegistry::Protection::DeleteTagRuleService, '#execute',
|
|||
described_class.new(container_protection_tag_rule, current_user: current_user).execute
|
||||
end
|
||||
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
shared_examples 'a successful service response' do
|
||||
it_behaves_like 'returning a success service response' do
|
||||
it 'contains the correct payload with no errors' do
|
||||
|
|
@ -91,4 +97,13 @@ RSpec.describe ContainerRegistry::Protection::DeleteTagRuleService, '#execute',
|
|||
|
||||
it { expect { service_execute }.to raise_error(ArgumentError) }
|
||||
end
|
||||
|
||||
context 'when the GitLab API is not supported' do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'an erroneous service response',
|
||||
message: 'GitLab container registry API not supported'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute', feature_category: :container_registry do
|
||||
include ContainerRegistryHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:current_user) { create(:user, maintainer_of: project) }
|
||||
let_it_be_with_reload(:container_protection_tag_rule) do
|
||||
|
|
@ -22,6 +24,10 @@ RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute',
|
|||
|
||||
subject(:service_execute) { service.execute }
|
||||
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
|
||||
end
|
||||
|
||||
shared_examples 'a successful service response' do
|
||||
let(:expected_attributes) { params }
|
||||
|
||||
|
|
@ -158,4 +164,13 @@ RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute',
|
|||
|
||||
it { expect { service_execute }.to raise_error(ArgumentError) }
|
||||
end
|
||||
|
||||
context 'when the GitLab API is not supported' do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'an erroneous service response',
|
||||
message: 'GitLab container registry API not supported'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'returning a mutation error' do |message|
|
||||
it 'returns an error from endpoint implementation (not from GraphQL framework)' do
|
||||
post_graphql_mutation_request.tap do
|
||||
expect_graphql_errors_to_be_empty
|
||||
expect(mutation_response['errors']).to match_array [message]
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not persisting changes'
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returning a GraphQL error' do |message|
|
||||
it 'returns a GraphQL error' do
|
||||
post_graphql_mutation_request.tap do
|
||||
expect_graphql_errors_to_include(message)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not persisting changes'
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'when user does not have permission' do
|
||||
context 'when user does not have permission' do
|
||||
let_it_be(:developer) { create(:user, developer_of: project) }
|
||||
let_it_be(:reporter) { create(:user, reporter_of: project) }
|
||||
let_it_be(:guest) { create(:user, guest_of: project) }
|
||||
let_it_be(:anonymous) { create(:user) }
|
||||
|
||||
where(:current_user) do
|
||||
[ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'returning a GraphQL error', /you don't have permission to perform this action/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'when feature flag container_registry_protected_tags is disabled' do
|
||||
context "when feature flag ':container_registry_protected_tags' disabled" do
|
||||
before do
|
||||
stub_feature_flags(container_registry_protected_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning a GraphQL error', /'container_registry_protected_tags' feature flag is disabled/
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'when the GitLab API is not supported' do
|
||||
context 'when the GitLab API is not supported' do
|
||||
before do
|
||||
stub_gitlab_api_client_to_support_gitlab_api(supported: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning a mutation error', 'GitLab container registry API not supported'
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue