Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-01-31 15:12:06 +00:00
parent e033caddff
commit a9b8a31684
59 changed files with 1642 additions and 527 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Ci
class NamespaceSettingPolicy < BasePolicy
delegate { @subject.namespace }
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: []

View File

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

View File

@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"pipelineVariablesDefaultRole": {
"type": "ENUM"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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