Add latest changes from gitlab-org/gitlab@master
|
@ -89,7 +89,7 @@ downtime_check:
|
|||
- rspec_profiling/
|
||||
- tmp/capybara/
|
||||
- tmp/memory_test/
|
||||
- junit_rspec.xml
|
||||
- log/*.log
|
||||
reports:
|
||||
junit: junit_rspec.xml
|
||||
|
||||
|
|
|
@ -328,7 +328,8 @@ export default {
|
|||
<button
|
||||
class="btn note-edit-cancel js-close-discussion-note-form"
|
||||
type="button"
|
||||
@click="cancelHandler()"
|
||||
data-testid="cancelBatchCommentsEnabled"
|
||||
@click="cancelHandler(true)"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
|
@ -353,7 +354,8 @@ export default {
|
|||
<button
|
||||
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
|
||||
type="button"
|
||||
@click="cancelHandler()"
|
||||
data-testid="cancel"
|
||||
@click="cancelHandler(true)"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// /dag is an alias for show
|
||||
import '../show/index';
|
|
@ -4,6 +4,17 @@
|
|||
}
|
||||
|
||||
table {
|
||||
/*
|
||||
* TODO
|
||||
* This is a temporary workaround until we fix the neutral
|
||||
* color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
|
||||
*
|
||||
* Remove this code as soon as this happens
|
||||
*/
|
||||
&.gl-table {
|
||||
@include gl-text-gray-700;
|
||||
}
|
||||
|
||||
&.table {
|
||||
margin-bottom: $gl-padding;
|
||||
|
||||
|
@ -32,8 +43,7 @@ table {
|
|||
}
|
||||
|
||||
th {
|
||||
background-color: $gray-light;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
@include gl-bg-gray-100;
|
||||
border-bottom: 0;
|
||||
|
||||
&.wide {
|
||||
|
|
|
@ -13,6 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
before_action do
|
||||
push_frontend_feature_flag(:junit_pipeline_view)
|
||||
push_frontend_feature_flag(:filter_pipelines_search)
|
||||
push_frontend_feature_flag(:dag_pipeline_tab)
|
||||
end
|
||||
before_action :ensure_pipeline, only: [:show]
|
||||
|
||||
|
@ -94,6 +95,10 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
render_show
|
||||
end
|
||||
|
||||
def dag
|
||||
render_show
|
||||
end
|
||||
|
||||
def failures
|
||||
if @pipeline.failed_builds.present?
|
||||
render_show
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Settings
|
||||
class AccessTokensController < Projects::ApplicationController
|
||||
include ProjectsHelper
|
||||
|
||||
before_action :check_feature_availability
|
||||
|
||||
def index
|
||||
@project_access_token = PersonalAccessToken.new
|
||||
set_index_vars
|
||||
end
|
||||
|
||||
def create
|
||||
token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
|
||||
|
||||
if token_response.success?
|
||||
@project_access_token = token_response.payload[:access_token]
|
||||
PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
|
||||
|
||||
redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
|
||||
else
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def revoke
|
||||
@project_access_token = finder.find(params[:id])
|
||||
revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
|
||||
|
||||
if revoked_response.success?
|
||||
flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
|
||||
else
|
||||
flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
|
||||
end
|
||||
|
||||
redirect_to namespace_project_settings_access_tokens_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_feature_availability
|
||||
render_404 unless project_access_token_available?(@project)
|
||||
end
|
||||
|
||||
def create_params
|
||||
params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
|
||||
end
|
||||
|
||||
def set_index_vars
|
||||
@scopes = Gitlab::Auth.resource_bot_scopes
|
||||
@active_project_access_tokens = finder(state: 'active').execute
|
||||
@inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute
|
||||
@new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
|
||||
end
|
||||
|
||||
def finder(options = {})
|
||||
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
|
||||
end
|
||||
|
||||
def bot_users
|
||||
@project.bots
|
||||
end
|
||||
|
||||
def key_identity
|
||||
"#{current_user.id}:#{@project.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -740,6 +740,12 @@ module ProjectsHelper
|
|||
Gitlab.config.registry.enabled &&
|
||||
can?(current_user, :destroy_container_image, project)
|
||||
end
|
||||
|
||||
def project_access_token_available?(project)
|
||||
return false if ::Gitlab.com?
|
||||
|
||||
::Feature.enabled?(:resource_access_token, project)
|
||||
end
|
||||
end
|
||||
|
||||
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
|
||||
|
|
|
@ -5,10 +5,31 @@ module Timebox
|
|||
|
||||
include AtomicInternalId
|
||||
include CacheMarkdownField
|
||||
include Gitlab::SQL::Pattern
|
||||
include IidRoutes
|
||||
include StripAttribute
|
||||
|
||||
TimeboxStruct = Struct.new(:title, :name, :id) do
|
||||
# Ensure these models match the interface required for exporting
|
||||
def serializable_hash(_opts = {})
|
||||
{ title: title, name: name, id: id }
|
||||
end
|
||||
end
|
||||
|
||||
# Represents a "No Timebox" state used for filtering Issues and Merge
|
||||
# Requests that have no timeboxes assigned.
|
||||
None = TimeboxStruct.new('No Timebox', 'No Timebox', 0)
|
||||
Any = TimeboxStruct.new('Any Timebox', '', -1)
|
||||
Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
|
||||
Started = TimeboxStruct.new('Started', '#started', -3)
|
||||
|
||||
included do
|
||||
# Defines the same constants above, but inside the including class.
|
||||
const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
|
||||
const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
|
||||
const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
|
||||
const_set :Started, TimeboxStruct.new('Started', '#started', -3)
|
||||
|
||||
alias_method :timebox_id, :id
|
||||
|
||||
validates :group, presence: true, unless: :project
|
||||
|
@ -35,6 +56,7 @@ module Timebox
|
|||
scope :active, -> { with_state(:active) }
|
||||
scope :closed, -> { with_state(:closed) }
|
||||
scope :for_projects, -> { where(group: nil).includes(:project) }
|
||||
scope :with_title, -> (title) { where(title: title) }
|
||||
|
||||
scope :for_projects_and_groups, -> (projects, groups) do
|
||||
projects = projects.compact if projects.is_a? Array
|
||||
|
@ -57,6 +79,50 @@ module Timebox
|
|||
alias_attribute :name, :title
|
||||
end
|
||||
|
||||
class_methods do
|
||||
# Searches for timeboxes with a matching title or description.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
fuzzy_search(query, [:title, :description])
|
||||
end
|
||||
|
||||
# Searches for timeboxes with a matching title.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search_title(query)
|
||||
fuzzy_search(query, [:title])
|
||||
end
|
||||
|
||||
def filter_by_state(timeboxes, state)
|
||||
case state
|
||||
when 'closed' then timeboxes.closed
|
||||
when 'all' then timeboxes
|
||||
else timeboxes.active
|
||||
end
|
||||
end
|
||||
|
||||
def count_by_state
|
||||
reorder(nil).group(:state).count
|
||||
end
|
||||
|
||||
def predefined_id?(id)
|
||||
[Any.id, None.id, Upcoming.id, Started.id].include?(id)
|
||||
end
|
||||
|
||||
def predefined?(timebox)
|
||||
predefined_id?(timebox&.id)
|
||||
end
|
||||
end
|
||||
|
||||
def title=(value)
|
||||
write_attribute(:title, sanitize_title(value)) if value.present?
|
||||
end
|
||||
|
|
|
@ -1,27 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Milestone < ApplicationRecord
|
||||
# Represents a "No Milestone" state used for filtering Issues and Merge
|
||||
# Requests that have no milestone assigned.
|
||||
MilestoneStruct = Struct.new(:title, :name, :id) do
|
||||
# Ensure these models match the interface required for exporting
|
||||
def serializable_hash(_opts = {})
|
||||
{ title: title, name: name, id: id }
|
||||
end
|
||||
end
|
||||
|
||||
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
|
||||
Any = MilestoneStruct.new('Any Milestone', '', -1)
|
||||
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
|
||||
Started = MilestoneStruct.new('Started', '#started', -3)
|
||||
|
||||
include Sortable
|
||||
include Referable
|
||||
include Timebox
|
||||
include Milestoneish
|
||||
include FromUnion
|
||||
include Importable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
|
@ -54,50 +39,6 @@ class Milestone < ApplicationRecord
|
|||
state :active
|
||||
end
|
||||
|
||||
class << self
|
||||
# Searches for milestones with a matching title or description.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
fuzzy_search(query, [:title, :description])
|
||||
end
|
||||
|
||||
# Searches for milestones with a matching title.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search_title(query)
|
||||
fuzzy_search(query, [:title])
|
||||
end
|
||||
|
||||
def filter_by_state(milestones, state)
|
||||
case state
|
||||
when 'closed' then milestones.closed
|
||||
when 'all' then milestones
|
||||
else milestones.active
|
||||
end
|
||||
end
|
||||
|
||||
def count_by_state
|
||||
reorder(nil).group(:state).count
|
||||
end
|
||||
|
||||
def predefined_id?(id)
|
||||
[Any.id, None.id, Upcoming.id, Started.id].include?(id)
|
||||
end
|
||||
|
||||
def predefined?(milestone)
|
||||
predefined_id?(milestone&.id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.reference_prefix
|
||||
'%'
|
||||
end
|
||||
|
|
|
@ -349,7 +349,7 @@ class Namespace < ApplicationRecord
|
|||
# We default to PlanLimits.new otherwise a lot of specs would fail
|
||||
# On production each plan should already have associated limits record
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/36037
|
||||
actual_plan.limits || PlanLimits.new
|
||||
actual_plan.actual_limits
|
||||
end
|
||||
|
||||
def actual_plan_name
|
||||
|
|
|
@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
|
|||
include Expirable
|
||||
include TokenAuthenticatable
|
||||
include Sortable
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
add_authentication_token_field :token, digest: true
|
||||
|
||||
|
@ -23,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord
|
|||
scope :without_impersonation, -> { where(impersonation: false) }
|
||||
scope :for_user, -> (user) { where(user: user) }
|
||||
scope :preload_users, -> { preload(:user) }
|
||||
scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
|
||||
scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
|
||||
|
||||
validates :scopes, presence: true
|
||||
validate :validate_scopes
|
||||
|
@ -39,12 +42,14 @@ class PersonalAccessToken < ApplicationRecord
|
|||
|
||||
def self.redis_getdel(user_id)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
encrypted_token = redis.get(redis_shared_state_key(user_id))
|
||||
redis.del(redis_shared_state_key(user_id))
|
||||
redis_key = redis_shared_state_key(user_id)
|
||||
encrypted_token = redis.get(redis_key)
|
||||
redis.del(redis_key)
|
||||
|
||||
begin
|
||||
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
|
||||
rescue => ex
|
||||
logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
|
||||
logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
|
||||
encrypted_token
|
||||
end
|
||||
end
|
||||
|
@ -58,6 +63,16 @@ class PersonalAccessToken < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
override :simple_sorts
|
||||
def self.simple_sorts
|
||||
super.merge(
|
||||
{
|
||||
'expires_at_asc' => -> { order_expires_at_asc },
|
||||
'expires_at_desc' => -> { order_expires_at_desc }
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate_scopes
|
||||
|
|
|
@ -26,6 +26,10 @@ class Plan < ApplicationRecord
|
|||
DEFAULT_PLANS
|
||||
end
|
||||
|
||||
def actual_limits
|
||||
self.limits || PlanLimits.new
|
||||
end
|
||||
|
||||
def default?
|
||||
self.class.default_plans.include?(name)
|
||||
end
|
||||
|
|
|
@ -1519,6 +1519,10 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def bots
|
||||
users.project_bot
|
||||
end
|
||||
|
||||
# Filters `users` to return only authorized users of the project
|
||||
def members_among(users)
|
||||
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resources
|
||||
class CreateAccessTokenService < BaseService
|
||||
attr_accessor :resource_type, :resource
|
||||
|
||||
def initialize(resource_type, resource, user, params = {})
|
||||
@resource_type = resource_type
|
||||
module ResourceAccessTokens
|
||||
class CreateService < BaseService
|
||||
def initialize(current_user, resource, params = {})
|
||||
@resource_type = resource.class.name.downcase
|
||||
@resource = resource
|
||||
@current_user = user
|
||||
@current_user = current_user
|
||||
@params = params.dup
|
||||
end
|
||||
|
||||
|
@ -33,6 +31,8 @@ module Resources
|
|||
|
||||
private
|
||||
|
||||
attr_reader :resource_type, :resource
|
||||
|
||||
def feature_enabled?
|
||||
::Feature.enabled?(:resource_access_token, resource)
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ module Resources
|
|||
|
||||
def personal_access_token_params
|
||||
{
|
||||
name: "#{resource_type}_bot",
|
||||
name: params[:name] || "#{resource_type}_bot",
|
||||
impersonation: false,
|
||||
scopes: params[:scopes] || default_scopes,
|
||||
expires_at: params[:expires_at] || nil
|
||||
|
@ -93,7 +93,7 @@ module Resources
|
|||
end
|
||||
|
||||
def default_scopes
|
||||
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
|
||||
Gitlab::Auth.resource_bot_scopes
|
||||
end
|
||||
|
||||
def provision_access(resource, user)
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ResourceAccessTokens
|
||||
class RevokeService < BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
RevokeAccessTokenError = Class.new(RuntimeError)
|
||||
|
||||
def initialize(current_user, resource, access_token)
|
||||
@current_user = current_user
|
||||
@access_token = access_token
|
||||
@bot_user = access_token.user
|
||||
@resource = resource
|
||||
end
|
||||
|
||||
def execute
|
||||
return error("Failed to find bot user") unless find_member
|
||||
|
||||
PersonalAccessToken.transaction do
|
||||
access_token.revoke!
|
||||
|
||||
raise RevokeAccessTokenError, "Failed to remove #{bot_user.name} member from: #{resource.name}" unless remove_member
|
||||
|
||||
raise RevokeAccessTokenError, "Migration to ghost user failed" unless migrate_to_ghost_user
|
||||
end
|
||||
|
||||
success("Revoked access token: #{access_token.name}")
|
||||
rescue ActiveRecord::ActiveRecordError, RevokeAccessTokenError => error
|
||||
log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
|
||||
error(error.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :access_token, :bot_user, :resource
|
||||
|
||||
def remove_member
|
||||
::Members::DestroyService.new(current_user).execute(find_member)
|
||||
end
|
||||
|
||||
def migrate_to_ghost_user
|
||||
::Users::MigrateToGhostUserService.new(bot_user).execute
|
||||
end
|
||||
|
||||
def find_member
|
||||
strong_memoize(:member) do
|
||||
if resource.is_a?(Project)
|
||||
resource.project_member(bot_user)
|
||||
elsif resource.is_a?(Group)
|
||||
resource.group_member(bot_user)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def error(message)
|
||||
ServiceResponse.error(message: message)
|
||||
end
|
||||
|
||||
def success(message)
|
||||
ServiceResponse.success(message: message)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
|
||||
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab)
|
||||
|
||||
.tabs-holder
|
||||
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
|
||||
|
@ -9,6 +10,10 @@
|
|||
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
|
||||
= _('Jobs')
|
||||
%span.badge.badge-pill.js-builds-counter= pipeline.total_size
|
||||
- if dag_pipeline_tab_enabled
|
||||
%li.js-dag-tab-link
|
||||
= link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
|
||||
= _('DAG')
|
||||
- if @pipeline.failed_builds.present?
|
||||
%li.js-failures-tab-link
|
||||
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
|
||||
|
@ -75,6 +80,9 @@
|
|||
%code.bash.js-build-output
|
||||
= build_summary(build)
|
||||
|
||||
- if dag_pipeline_tab_enabled
|
||||
#js-tab-dag.tab-pane
|
||||
|
||||
#js-tab-tests.tab-pane
|
||||
#js-pipeline-tests-detail
|
||||
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Increase constrast ratio of text in some tables
|
||||
merge_request: 30903
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support limits for offset based pagination
|
||||
merge_request: 28460
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed cancel reply button not alerting the user
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -15,6 +15,7 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
|
|||
post :cancel
|
||||
post :retry
|
||||
get :builds
|
||||
get :dag
|
||||
get :failures
|
||||
get :status
|
||||
get :test_report
|
||||
|
|
|
@ -90,6 +90,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
post :create_deploy_token, path: 'deploy_token/create'
|
||||
post :cleanup
|
||||
end
|
||||
|
||||
resources :access_tokens, only: [:index, :create] do
|
||||
member do
|
||||
put :revoke
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :autocomplete_sources, only: [] do
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddOffsetPaginationPlanLimit < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :plan_limits, :offset_pagination_limit, :integer, default: 50000, null: false
|
||||
end
|
||||
end
|
|
@ -4793,7 +4793,8 @@ CREATE TABLE public.plan_limits (
|
|||
project_hooks integer DEFAULT 100 NOT NULL,
|
||||
group_hooks integer DEFAULT 50 NOT NULL,
|
||||
ci_project_subscriptions integer DEFAULT 2 NOT NULL,
|
||||
ci_pipeline_schedules integer DEFAULT 10 NOT NULL
|
||||
ci_pipeline_schedules integer DEFAULT 10 NOT NULL,
|
||||
offset_pagination_limit integer DEFAULT 50000 NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.plan_limits_id_seq
|
||||
|
@ -13603,6 +13604,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200415161021
|
||||
20200415161206
|
||||
20200415192656
|
||||
20200415203024
|
||||
20200416005331
|
||||
20200416111111
|
||||
20200416120128
|
||||
|
|
|
@ -99,6 +99,29 @@ header. Such emails don't create comments on issues or merge requests.
|
|||
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
|
||||
and to limit memory consumption.
|
||||
|
||||
## Max offset allowed via REST API for offset-based pagination
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
|
||||
|
||||
When using offset-based pagination in the REST API, there is a limit to the maximum
|
||||
requested offset into the set of results. This limit is only applied to endpoints that
|
||||
support keyset-based pagination. More information about pagination options can be
|
||||
found in the [API docs section on pagination](../api/README.md#pagination).
|
||||
|
||||
To set this limit on a self-managed installation, run the following in the
|
||||
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
|
||||
|
||||
```ruby
|
||||
# If limits don't exist for the default plan, you can create one with:
|
||||
# Plan.default.create_limits!
|
||||
|
||||
Plan.default.limits.update!(offset_pagination_limit: 10000)
|
||||
```
|
||||
|
||||
- **Default offset pagination limit:** 50000
|
||||
|
||||
NOTE: **Note:** Set the limit to `0` to disable it.
|
||||
|
||||
## CI/CD limits
|
||||
|
||||
### Number of jobs in active pipelines
|
||||
|
|
|
@ -249,3 +249,30 @@ following command:
|
|||
```ruby
|
||||
Feature.enable(:junit_pipeline_view)
|
||||
```
|
||||
|
||||
## Viewing JUnit screenshots on GitLab
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
|
||||
|
||||
If JUnit XML files contain an `attachment` tag, GitLab parses the attachment.
|
||||
|
||||
Upload your screenshots as [artifacts](pipelines/job_artifacts.md#artifactsreportsjunit) to GitLab. The `attachment` tag **must** contain the absolute path to the screenshots you uploaded.
|
||||
|
||||
```xml
|
||||
<testcase time="1.00" name="Test">
|
||||
<system-out>[[ATTACHMENT|/absolute/path/to/some/file]]</system-out>
|
||||
</testcase>
|
||||
```
|
||||
|
||||
When [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/6061) is complete, the attached file will be visible on the pipeline details page.
|
||||
|
||||
### Enabling the feature
|
||||
|
||||
This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.
|
||||
|
||||
To enable this feature, ask a GitLab administrator with [Rails console access](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
|
||||
following command:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:junit_pipeline_screenshots_view)
|
||||
```
|
||||
|
|
|
@ -565,6 +565,44 @@ to mix types, that is also possible, as long as you don't mix items at the same
|
|||
- Unordered list item three.
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
Tables should be used to describe complex information in a straightforward
|
||||
manner. Note that in many cases, an unordered list is sufficient to describe a
|
||||
list of items with a single, simple description per item. But, if you have data
|
||||
that is best described by a matrix, tables are the best choice for use.
|
||||
|
||||
### Creation guidelines
|
||||
|
||||
Due to accessibility and scanability requirements, tables should not have any
|
||||
empty cells. If there is no otherwise meaningful value for a cell, consider entering
|
||||
*N/A* (for 'not applicable') or *none*.
|
||||
|
||||
To help tables be easier to maintain, consider adding additional spaces to the
|
||||
column widths to make them consistent. For example:
|
||||
|
||||
```markdown
|
||||
| App name | Description | Requirements |
|
||||
|:---------|:---------------------|:---------------|
|
||||
| App 1 | Description text 1. | Requirements 1 |
|
||||
| App 2 | Description text 2. | None |
|
||||
```
|
||||
|
||||
Consider installing a plugin or extension in your editor for formatting tables:
|
||||
|
||||
- [Markdown Table Prettifier](https://marketplace.visualstudio.com/items?itemName=darkriszty.markdown-table-prettify) for Visual Studio Code
|
||||
- [Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) for Sublime Text
|
||||
- [Markdown Table Formatter](https://atom.io/packages/markdown-table-formatter) for Atom
|
||||
|
||||
### Feature tables
|
||||
|
||||
When creating tables of lists of features (such as whether or not features are
|
||||
available to certain roles on the [Permissions](../../user/permissions.md#project-members-permissions)
|
||||
page), use the following phrases (based on the SVG icons):
|
||||
|
||||
- *No*: **{dotted-circle}** No
|
||||
- *Yes*: **{check-circle}** Yes
|
||||
|
||||
## Quotes
|
||||
|
||||
Valid for Markdown content only, not for frontmatter entries:
|
||||
|
|
|
@ -379,7 +379,8 @@ Widgets should now be replicated by Geo!
|
|||
1. Update `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md` with new fields.
|
||||
1. Update `ee/spec/models/geo_node_status_spec.rb` and `ee/spec/factories/geo_node_statuses.rb` with new fields.
|
||||
|
||||
To do: Add verification on secondaries.
|
||||
To do: Add verification on secondaries. This should be done as part of
|
||||
[Geo: Self Service Framework - First Implementation for Package File verification](https://gitlab.com/groups/gitlab-org/-/epics/1817)
|
||||
|
||||
Widgets should now be verified by Geo!
|
||||
|
||||
|
@ -505,7 +506,8 @@ via the GraphQL API!
|
|||
|
||||
#### Admin UI
|
||||
|
||||
To do.
|
||||
To do: This should be done as part of
|
||||
[Geo: Implement frontend for Self-Service Framework replicables](https://gitlab.com/groups/gitlab-org/-/epics/2525)
|
||||
|
||||
Widget sync and verification data (aggregate and individual) should now be
|
||||
available in the Admin UI!
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
---
|
||||
description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here.
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
||||
# Code Review Analytics **(STARTER)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Analytics
|
||||
|
||||
## Analytics workspace
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Productivity Analytics **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Value Stream Analytics
|
||||
|
||||
> - Introduced as Cycle Analytics prior to GitLab 12.3 at the project level.
|
||||
|
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 68 KiB |
|
@ -30,12 +30,20 @@ will be displayed in the merge request area. That is the case when you add the
|
|||
Consecutive merge requests will have something to compare to and the license
|
||||
compliance report will be shown properly.
|
||||
|
||||
![License Compliance Widget](img/license_compliance.png)
|
||||
![License Compliance Widget](img/license_compliance_v13_0.png)
|
||||
|
||||
If you are a project or group Maintainer, you can click on a license to be given
|
||||
the choice to allow it or deny it.
|
||||
|
||||
![License approval decision](img/license_compliance_decision.png)
|
||||
![License approval decision](img/license_compliance_decision_v13_0.png)
|
||||
|
||||
When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
|
||||
|
||||
![License List](img/license_list_v13_0.png)
|
||||
|
||||
You can view and modify existing policies from the [policies](#policies) tab.
|
||||
|
||||
![Edit Policy](img/policies_maintainer_edit_v13_0.png)
|
||||
|
||||
## Use cases
|
||||
|
||||
|
@ -402,7 +410,7 @@ To allow or deny a license:
|
|||
**License Compliance** section.
|
||||
1. Click the **Add a license** button.
|
||||
|
||||
![License Compliance Add License](img/license_compliance_add_license_v12_3.png)
|
||||
![License Compliance Add License](img/license_compliance_add_license_v13_0.png)
|
||||
|
||||
1. In the **License name** dropdown, either:
|
||||
- Select one of the available licenses. You can search for licenses in the field
|
||||
|
@ -416,13 +424,13 @@ To modify an existing license:
|
|||
|
||||
1. In the **License Compliance** list, click the **Allow/Deny** dropdown to change it to the desired status.
|
||||
|
||||
![License Compliance Settings](img/license_compliance_settings_v12_3.png)
|
||||
![License Compliance Settings](img/license_compliance_settings_v13_0.png)
|
||||
|
||||
Searching for Licenses:
|
||||
|
||||
1. Use the **Search** box to search for a specific license.
|
||||
|
||||
![License Compliance Search](img/license_compliance_search_v12_3.png)
|
||||
![License Compliance Search](img/license_compliance_search_v13_0.png)
|
||||
|
||||
## License Compliance report under pipelines
|
||||
|
||||
|
@ -465,8 +473,9 @@ in your project's sidebar, and you'll see the licenses displayed, where:
|
|||
|
||||
- **Name:** The name of the license.
|
||||
- **Component:** The components which have this license.
|
||||
- **Policy Violation:** The license has a [license policy](#policies) marked as **Deny**.
|
||||
|
||||
![License List](img/license_list_v12_6.png)
|
||||
![License List](img/license_list_v13_0.png)
|
||||
|
||||
## Policies
|
||||
|
||||
|
@ -477,9 +486,9 @@ and the associated classifications for each.
|
|||
|
||||
Policies can be configured by maintainers of the project.
|
||||
|
||||
![Edit Policy](img/policies_maintainer_edit_v12_9.png)
|
||||
![Add Policy](img/policies_maintainer_add_v12_9.png)
|
||||
![Edit Policy](img/policies_maintainer_edit_v13_0.png)
|
||||
![Add Policy](img/policies_maintainer_add_v13_0.png)
|
||||
|
||||
Developers of the project can view the policies configured in a project.
|
||||
|
||||
![View Policies](img/policies_v12_9.png)
|
||||
![View Policies](img/policies_v13_0.png)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
---
|
||||
type: reference
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Contribution Analytics **(STARTER)**
|
||||
|
||||
> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
---
|
||||
type: reference, howto
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Insights **(ULTIMATE)**
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
---
|
||||
type: reference
|
||||
stage: Manage
|
||||
group: Analytics
|
||||
To determine the technical writer assigned to the Stage/Group associated with this page, see:
|
||||
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Issues Analytics **(PREMIUM)**
|
||||
|
|
|
@ -3,19 +3,24 @@
|
|||
module API
|
||||
module Helpers
|
||||
module PaginationStrategies
|
||||
def paginate_with_strategies(relation)
|
||||
paginator = paginator(relation)
|
||||
def paginate_with_strategies(relation, request_scope)
|
||||
paginator = paginator(relation, request_scope)
|
||||
|
||||
yield(paginator.paginate(relation)).tap do |records, _|
|
||||
paginator.finalize(records)
|
||||
end
|
||||
end
|
||||
|
||||
def paginator(relation)
|
||||
return Gitlab::Pagination::OffsetPagination.new(self) unless keyset_pagination_enabled?
|
||||
def paginator(relation, request_scope = nil)
|
||||
return keyset_paginator(relation) if keyset_pagination_enabled?
|
||||
|
||||
offset_paginator(relation, request_scope)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyset_paginator(relation)
|
||||
request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
|
||||
|
||||
unless Gitlab::Pagination::Keyset.available?(request_context, relation)
|
||||
return error!('Keyset pagination is not yet available for this type of request', 405)
|
||||
end
|
||||
|
@ -23,11 +28,28 @@ module API
|
|||
Gitlab::Pagination::Keyset::Pager.new(request_context)
|
||||
end
|
||||
|
||||
private
|
||||
def offset_paginator(relation, request_scope)
|
||||
offset_limit = limit_for_scope(request_scope)
|
||||
if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit)
|
||||
return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
|
||||
"for requests that return objects of type #{relation.klass}. " \
|
||||
"Remaining records can be retrieved using keyset pagination.", 405)
|
||||
end
|
||||
|
||||
Gitlab::Pagination::OffsetPagination.new(self)
|
||||
end
|
||||
|
||||
def keyset_pagination_enabled?
|
||||
params[:pagination] == 'keyset'
|
||||
end
|
||||
|
||||
def limit_for_scope(scope)
|
||||
(scope || Plan.default).actual_limits.offset_pagination_limit
|
||||
end
|
||||
|
||||
def offset_limit_exceeded?(offset_limit)
|
||||
offset_limit.positive? && params[:page] * params[:per_page] > offset_limit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,7 +95,7 @@ module API
|
|||
projects = reorder_projects(projects)
|
||||
projects = apply_filters(projects)
|
||||
|
||||
records, options = paginate_with_strategies(projects) do |projects|
|
||||
records, options = paginate_with_strategies(projects, options[:request_scope]) do |projects|
|
||||
projects, options = with_custom_attributes(projects, options)
|
||||
|
||||
options = options.reverse_merge(
|
||||
|
@ -313,7 +313,7 @@ module API
|
|||
get ':id/forks' do
|
||||
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
|
||||
|
||||
present_projects forks
|
||||
present_projects forks, request_scope: user_project
|
||||
end
|
||||
|
||||
desc 'Check pages access of this project'
|
||||
|
|
|
@ -9,7 +9,7 @@ module Banzai
|
|||
# Examples:
|
||||
# Pipeline[nil] # => Banzai::Pipeline::FullPipeline
|
||||
# Pipeline[:label] # => Banzai::Pipeline::LabelPipeline
|
||||
# Pipeline[StatusPage::PostProcessPipeline] # => StatusPage::PostProcessPipeline
|
||||
# Pipeline[StatusPage::Pipeline::PostProcessPipeline] # => StatusPage::Pipeline::PostProcessPipeline
|
||||
#
|
||||
# Pipeline['label'] # => raises ArgumentError - unsupport type
|
||||
# Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline
|
||||
|
|
|
@ -138,15 +138,18 @@ module Banzai
|
|||
#
|
||||
# html - String to process
|
||||
# context - Hash of options to customize output
|
||||
# :pipeline - Symbol pipeline type
|
||||
# :pipeline - Symbol pipeline type - for context transform only, defaults to :full
|
||||
# :project - Project
|
||||
# :user - User object
|
||||
# :post_process_pipeline - pipeline to use for post_processing - defaults to PostProcessPipeline
|
||||
#
|
||||
# Returns an HTML-safe String
|
||||
def self.post_process(html, context)
|
||||
context = Pipeline[context[:pipeline]].transform_context(context)
|
||||
|
||||
pipeline = Pipeline[:post_process]
|
||||
# Use a passed class for the pipeline or default to PostProcessPipeline
|
||||
pipeline = context.delete(:post_process_pipeline) || ::Banzai::Pipeline::PostProcessPipeline
|
||||
|
||||
if context[:xhtml]
|
||||
pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
|
||||
else
|
||||
|
|
|
@ -337,6 +337,10 @@ module Gitlab
|
|||
REGISTRY_SCOPES
|
||||
end
|
||||
|
||||
def resource_bot_scopes
|
||||
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def non_admin_available_scopes
|
||||
|
|
|
@ -3,11 +3,18 @@
|
|||
module Gitlab
|
||||
module Pagination
|
||||
module Keyset
|
||||
SUPPORTED_TYPES = [
|
||||
Project
|
||||
].freeze
|
||||
|
||||
def self.available_for_type?(relation)
|
||||
SUPPORTED_TYPES.include?(relation.klass)
|
||||
end
|
||||
|
||||
def self.available?(request_context, relation)
|
||||
order_by = request_context.page.order_by
|
||||
|
||||
# This is only available for Project and order-by id (asc/desc)
|
||||
return false unless relation.klass == Project
|
||||
return false unless available_for_type?(relation)
|
||||
return false unless order_by.size == 1 && order_by[:id]
|
||||
|
||||
true
|
||||
|
|
|
@ -6072,6 +6072,9 @@ msgstr ""
|
|||
msgid "Could not revoke personal access token %{personal_access_token_name}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not revoke project access token %{project_access_token_name}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not save group ID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6620,6 +6623,9 @@ msgstr ""
|
|||
msgid "CycleAnalytics|stage dropdown"
|
||||
msgstr ""
|
||||
|
||||
msgid "DAG"
|
||||
msgstr ""
|
||||
|
||||
msgid "DNS"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17796,6 +17802,9 @@ msgstr ""
|
|||
msgid "Revoked personal access token %{personal_access_token_name}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Revoked project access token %{project_access_token_name}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "RightSidebar|adding a"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24521,6 +24530,9 @@ msgstr ""
|
|||
msgid "Your new personal access token has been created."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your new project access token has been created."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require('spec_helper')
|
||||
|
||||
describe Projects::Settings::AccessTokensController do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
shared_examples 'feature unavailability' do
|
||||
context 'when flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(resource_access_token: false)
|
||||
end
|
||||
|
||||
it { is_expected.to have_gitlab_http_status(:not_found) }
|
||||
end
|
||||
|
||||
context 'when environment is Gitlab.com' do
|
||||
before do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to have_gitlab_http_status(:not_found) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#index' do
|
||||
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
|
||||
|
||||
it_behaves_like 'feature unavailability'
|
||||
|
||||
context 'when feature is available' do
|
||||
let_it_be(:bot_user) { create(:user, :project_bot) }
|
||||
let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
|
||||
let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(bot_user)
|
||||
end
|
||||
|
||||
before do
|
||||
enable_feature
|
||||
end
|
||||
|
||||
it 'retrieves active project access tokens' do
|
||||
subject
|
||||
|
||||
expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
|
||||
end
|
||||
|
||||
it 'retrieves inactive project access tokens' do
|
||||
subject
|
||||
|
||||
expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
|
||||
end
|
||||
|
||||
it 'lists all available scopes' do
|
||||
subject
|
||||
|
||||
expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
|
||||
end
|
||||
|
||||
it 'retrieves newly created personal access token value' do
|
||||
token_value = 'random-value'
|
||||
allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
|
||||
|
||||
subject
|
||||
|
||||
expect(assigns(:new_project_access_token)).to eq(token_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create', :clean_gitlab_redis_shared_state do
|
||||
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
|
||||
|
||||
let_it_be(:access_token_params) { {} }
|
||||
|
||||
it_behaves_like 'feature unavailability'
|
||||
|
||||
context 'when feature is available' do
|
||||
let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } }
|
||||
|
||||
before do
|
||||
enable_feature
|
||||
end
|
||||
|
||||
def created_token
|
||||
PersonalAccessToken.order(:created_at).last
|
||||
end
|
||||
|
||||
it 'returns success message' do
|
||||
subject
|
||||
|
||||
expect(response.flash[:notice]).to match(/\AYour new project access token has been created./i)
|
||||
end
|
||||
|
||||
it 'creates project access token' do
|
||||
subject
|
||||
|
||||
expect(created_token.name).to eq(access_token_params[:name])
|
||||
expect(created_token.scopes).to eq(access_token_params[:scopes])
|
||||
expect(created_token.expires_at).to eq(access_token_params[:expires_at])
|
||||
end
|
||||
|
||||
it 'creates project bot user' do
|
||||
subject
|
||||
|
||||
expect(created_token.user).to be_project_bot
|
||||
end
|
||||
|
||||
it 'stores newly created token redis store' do
|
||||
expect(PersonalAccessToken).to receive(:redis_store!)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect { subject }.to change { User.count }.by(1) }
|
||||
it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
|
||||
|
||||
context 'when unsuccessful' do
|
||||
before do
|
||||
allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
|
||||
allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
|
||||
end
|
||||
end
|
||||
|
||||
it { expect(subject).to render_template(:index) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#revoke' do
|
||||
subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
|
||||
|
||||
let_it_be(:bot_user) { create(:user, :project_bot) }
|
||||
let_it_be(:project_access_token) { create(:personal_access_token, user: bot_user) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(bot_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'feature unavailability'
|
||||
|
||||
context 'when feature is available' do
|
||||
before do
|
||||
enable_feature
|
||||
end
|
||||
|
||||
it 'revokes token access' do
|
||||
subject
|
||||
|
||||
expect(project_access_token.reload.revoked?).to be true
|
||||
end
|
||||
|
||||
it 'removed membership of bot user' do
|
||||
subject
|
||||
|
||||
expect(project.reload.bots).not_to include(bot_user)
|
||||
end
|
||||
|
||||
it 'blocks project bot user' do
|
||||
subject
|
||||
|
||||
expect(bot_user.reload.blocked?).to be true
|
||||
end
|
||||
|
||||
it 'converts issuables of the bot user to ghost user' do
|
||||
issue = create(:issue, author: bot_user)
|
||||
|
||||
subject
|
||||
|
||||
expect(issue.reload.author.ghost?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def enable_feature
|
||||
allow(Gitlab).to receive(:com?).and_return(false)
|
||||
stub_feature_flags(resource_access_token: true)
|
||||
end
|
||||
end
|
|
@ -235,7 +235,9 @@ describe 'Merge request > User posts diff notes', :js do
|
|||
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
|
||||
write_comment_on_line(line_holder, diff_side)
|
||||
|
||||
find('.js-close-discussion-note-form').click
|
||||
accept_confirm do
|
||||
find('.js-close-discussion-note-form').click
|
||||
end
|
||||
|
||||
assert_comment_dismissal(line_holder)
|
||||
end
|
||||
|
|
|
@ -147,7 +147,10 @@ describe 'Merge request > User posts notes', :js do
|
|||
it 'resets the edit note form textarea with the original content of the note if cancelled' do
|
||||
within('.current-note-edit-form') do
|
||||
fill_in 'note[note]', with: 'Some new content'
|
||||
find('.btn-cancel').click
|
||||
|
||||
accept_confirm do
|
||||
find('.btn-cancel').click
|
||||
end
|
||||
end
|
||||
expect(find('.js-note-text').text).to eq ''
|
||||
end
|
||||
|
|
|
@ -327,9 +327,10 @@ describe 'Pipeline', :js do
|
|||
visit_pipeline
|
||||
end
|
||||
|
||||
it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
|
||||
it 'shows Pipeline, Jobs, DAG and Failed Jobs tabs with link' do
|
||||
expect(page).to have_link('Pipeline')
|
||||
expect(page).to have_link('Jobs')
|
||||
expect(page).to have_link('DAG')
|
||||
expect(page).to have_link('Failed Jobs')
|
||||
end
|
||||
|
||||
|
@ -614,6 +615,20 @@ describe 'Pipeline', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FF dag_pipeline_tab is disabled' do
|
||||
before do
|
||||
stub_feature_flags(dag_pipeline_tab: false)
|
||||
visit_pipeline
|
||||
end
|
||||
|
||||
it 'does not show DAG link' do
|
||||
expect(page).to have_link('Pipeline')
|
||||
expect(page).to have_link('Jobs')
|
||||
expect(page).not_to have_link('DAG')
|
||||
expect(page).to have_link('Failed Jobs')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to read jobs' do
|
||||
|
@ -865,9 +880,10 @@ describe 'Pipeline', :js do
|
|||
end
|
||||
|
||||
context 'page tabs' do
|
||||
it 'shows Pipeline and Jobs tabs with link' do
|
||||
it 'shows Pipeline, Jobs and DAG tabs with link' do
|
||||
expect(page).to have_link('Pipeline')
|
||||
expect(page).to have_link('Jobs')
|
||||
expect(page).to have_link('DAG')
|
||||
end
|
||||
|
||||
it 'shows counter in Jobs tab' do
|
||||
|
@ -1057,6 +1073,37 @@ describe 'Pipeline', :js do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /:project/pipelines/:id/dag' do
|
||||
include_context 'pipeline builds'
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
||||
|
||||
before do
|
||||
visit dag_project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
it 'shows DAG tab pane as active' do
|
||||
expect(page).to have_css('#js-tab-dag.active', visible: false)
|
||||
end
|
||||
|
||||
context 'page tabs' do
|
||||
it 'shows Pipeline, Jobs and DAG tabs with link' do
|
||||
expect(page).to have_link('Pipeline')
|
||||
expect(page).to have_link('Jobs')
|
||||
expect(page).to have_link('DAG')
|
||||
end
|
||||
|
||||
it 'shows counter in Jobs tab' do
|
||||
expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
|
||||
end
|
||||
|
||||
it 'shows DAG tab as active' do
|
||||
expect(page).to have_css('li.js-dag-tab-link .active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user sees pipeline flags in a pipeline detail page' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
|
|
|
@ -161,18 +161,18 @@ describe('issue_note_form component', () => {
|
|||
|
||||
describe('actions', () => {
|
||||
it('should be possible to cancel', () => {
|
||||
// TODO: do not spy on vm
|
||||
jest.spyOn(wrapper.vm, 'cancelHandler');
|
||||
const cancelHandler = jest.fn();
|
||||
wrapper.setProps({
|
||||
...props,
|
||||
isEditing: true,
|
||||
});
|
||||
wrapper.setMethods({ cancelHandler });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
const cancelButton = wrapper.find('.note-edit-cancel');
|
||||
const cancelButton = wrapper.find('[data-testid="cancel"]');
|
||||
cancelButton.trigger('click');
|
||||
|
||||
expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
|
||||
expect(cancelHandler).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ describe API::Helpers::PaginationStrategies do
|
|||
subject { Class.new.include(described_class).new }
|
||||
|
||||
let(:expected_result) { double("result") }
|
||||
let(:relation) { double("relation") }
|
||||
let(:relation) { double("relation", klass: "SomeClass") }
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
|
@ -17,18 +17,18 @@ describe API::Helpers::PaginationStrategies do
|
|||
let(:paginator) { double("paginator", paginate: expected_result, finalize: nil) }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:paginator).with(relation).and_return(paginator)
|
||||
allow(subject).to receive(:paginator).with(relation, nil).and_return(paginator)
|
||||
end
|
||||
|
||||
it 'yields paginated relation' do
|
||||
expect { |b| subject.paginate_with_strategies(relation, &b) }.to yield_with_args(expected_result)
|
||||
expect { |b| subject.paginate_with_strategies(relation, nil, &b) }.to yield_with_args(expected_result)
|
||||
end
|
||||
|
||||
it 'calls #finalize with first value returned from block' do
|
||||
return_value = double
|
||||
expect(paginator).to receive(:finalize).with(return_value)
|
||||
|
||||
subject.paginate_with_strategies(relation) do |records|
|
||||
subject.paginate_with_strategies(relation, nil) do |records|
|
||||
some_options = {}
|
||||
[return_value, some_options]
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ describe API::Helpers::PaginationStrategies do
|
|||
it 'returns whatever the block returns' do
|
||||
return_value = [double, double]
|
||||
|
||||
result = subject.paginate_with_strategies(relation) do |records|
|
||||
result = subject.paginate_with_strategies(relation, nil) do |records|
|
||||
return_value
|
||||
end
|
||||
|
||||
|
@ -47,16 +47,77 @@ describe API::Helpers::PaginationStrategies do
|
|||
|
||||
describe '#paginator' do
|
||||
context 'offset pagination' do
|
||||
let(:plan_limits) { Plan.default.actual_limits }
|
||||
let(:offset_limit) { plan_limits.offset_pagination_limit }
|
||||
let(:paginator) { double("paginator") }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:keyset_pagination_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'delegates to OffsetPagination' do
|
||||
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
|
||||
context 'when keyset pagination is available for the relation' do
|
||||
before do
|
||||
allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(true)
|
||||
end
|
||||
|
||||
expect(subject.paginator(relation)).to eq(paginator)
|
||||
context 'when a request scope is given' do
|
||||
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
|
||||
let(:request_scope) { double("scope", actual_limits: plan_limits) }
|
||||
|
||||
context 'when the scope limit is exceeded' do
|
||||
it 'renders a 405 error' do
|
||||
expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
|
||||
|
||||
subject.paginator(relation, request_scope)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the scope limit is not exceeded' do
|
||||
let(:params) { { per_page: 100, page: offset_limit / 100 } }
|
||||
|
||||
it 'delegates to OffsetPagination' do
|
||||
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
|
||||
|
||||
expect(subject.paginator(relation, request_scope)).to eq(paginator)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a request scope is not given' do
|
||||
context 'when the default limits are exceeded' do
|
||||
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
|
||||
|
||||
it 'renders a 405 error' do
|
||||
expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
|
||||
|
||||
subject.paginator(relation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the default limits are not exceeded' do
|
||||
let(:params) { { per_page: 100, page: offset_limit / 100 } }
|
||||
|
||||
it 'delegates to OffsetPagination' do
|
||||
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
|
||||
|
||||
expect(subject.paginator(relation)).to eq(paginator)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when keyset pagination is not available for the relation' do
|
||||
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(false)
|
||||
end
|
||||
|
||||
it 'delegates to OffsetPagination' do
|
||||
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
|
||||
|
||||
expect(subject.paginator(relation)).to eq(paginator)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::Renderer do
|
||||
let(:renderer) { described_class }
|
||||
|
||||
def fake_object(fresh:)
|
||||
object = double('object')
|
||||
|
||||
|
@ -40,8 +42,6 @@ describe Banzai::Renderer do
|
|||
end
|
||||
|
||||
describe '#render_field' do
|
||||
let(:renderer) { described_class }
|
||||
|
||||
context 'without cache' do
|
||||
let(:commit) { fake_cacheless_object }
|
||||
|
||||
|
@ -83,4 +83,57 @@ describe Banzai::Renderer do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#post_process' do
|
||||
let(:context_options) { {} }
|
||||
let(:html) { 'Consequatur aperiam et nesciunt modi aut assumenda quo id. '}
|
||||
let(:post_processed_html) { double(html_safe: 'safe doc') }
|
||||
let(:doc) { double(to_html: post_processed_html) }
|
||||
|
||||
subject { renderer.post_process(html, context_options) }
|
||||
|
||||
context 'when xhtml' do
|
||||
let(:context_options) { { xhtml: ' ' } }
|
||||
|
||||
context 'without :post_process_pipeline key' do
|
||||
it 'uses PostProcessPipeline' do
|
||||
expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).and_return(doc)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :post_process_pipeline key' do
|
||||
let(:context_options) { { post_process_pipeline: Object, xhtml: ' ' } }
|
||||
|
||||
it 'uses passed post process pipeline' do
|
||||
expect(Object).to receive(:to_document).and_return(doc)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not xhtml' do
|
||||
context 'without :post_process_pipeline key' do
|
||||
it 'uses PostProcessPipeline' do
|
||||
expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_html)
|
||||
.with(html, { only_path: true, disable_asset_proxy: true })
|
||||
.and_return(post_processed_html)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :post_process_pipeline key' do
|
||||
let(:context_options) { { post_process_pipeline: Object } }
|
||||
|
||||
it 'uses passed post process pipeline' do
|
||||
expect(Object).to receive(:to_html).and_return(post_processed_html)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -715,6 +715,14 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
end
|
||||
|
||||
describe ".resource_bot_scopes" do
|
||||
subject { described_class.resource_bot_scopes }
|
||||
|
||||
it { is_expected.to include(*described_class::API_SCOPES - [:read_user]) }
|
||||
it { is_expected.to include(*described_class::REPOSITORY_SCOPES) }
|
||||
it { is_expected.to include(*described_class.registry_scopes) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expect_results_with_abilities(personal_access_token, abilities, success = true)
|
||||
|
|
|
@ -3,6 +3,18 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Pagination::Keyset do
|
||||
describe '.available_for_type?' do
|
||||
subject { described_class }
|
||||
|
||||
it 'returns true for Project' do
|
||||
expect(subject.available_for_type?(Project.all)).to be_truthy
|
||||
end
|
||||
|
||||
it 'return false for other types of relations' do
|
||||
expect(subject.available_for_type?(User.all)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '.available?' do
|
||||
subject { described_class }
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ describe Milestone do
|
|||
it_behaves_like 'a timebox', :milestone
|
||||
|
||||
describe 'MilestoneStruct#serializable_hash' do
|
||||
let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
|
||||
let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
|
||||
|
||||
it 'presents the predefined milestone as a hash' do
|
||||
expect(predefined_milestone.serializable_hash).to eq(
|
||||
|
|
|
@ -179,4 +179,27 @@ describe PersonalAccessToken do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.simple_sorts' do
|
||||
it 'includes overriden keys' do
|
||||
expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ordering by expires_at' do
|
||||
let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
|
||||
let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
|
||||
|
||||
describe '.order_expires_at_asc' do
|
||||
it 'returns ordered list in asc order of expiry date' do
|
||||
expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
|
||||
end
|
||||
end
|
||||
|
||||
describe '.order_expires_at_desc' do
|
||||
it 'returns ordered list in desc order of expiry date' do
|
||||
expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6081,6 +6081,23 @@ describe Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#bots' do
|
||||
subject { project.bots }
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:project_bot) { create(:user, :project_bot) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before_all do
|
||||
[project_bot, user].each do |member|
|
||||
project.add_maintainer(member)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(project_bot) }
|
||||
it { is_expected.not_to include(user) }
|
||||
end
|
||||
|
||||
def finish_job(export_job)
|
||||
export_job.start
|
||||
export_job.finish
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Resources::CreateAccessTokenService do
|
||||
subject { described_class.new(resource_type, resource, user, params).execute }
|
||||
describe ResourceAccessTokens::CreateService do
|
||||
subject { described_class.new(user, resource, params).execute }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
|
@ -12,7 +12,7 @@ describe Resources::CreateAccessTokenService do
|
|||
describe '#execute' do
|
||||
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
|
||||
shared_examples 'fails when user does not have the permission to create a Resource Bot' do
|
||||
before do
|
||||
before_all do
|
||||
resource.add_developer(user)
|
||||
end
|
||||
|
||||
|
@ -56,7 +56,7 @@ describe Resources::CreateAccessTokenService do
|
|||
end
|
||||
|
||||
context 'when user provides value' do
|
||||
let(:params) { { name: 'Random bot' } }
|
||||
let_it_be(:params) { { name: 'Random bot' } }
|
||||
|
||||
it 'overrides the default value' do
|
||||
response = subject
|
||||
|
@ -83,12 +83,12 @@ describe Resources::CreateAccessTokenService do
|
|||
response = subject
|
||||
access_token = response.payload[:access_token]
|
||||
|
||||
expect(access_token.scopes).to eq(Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user])
|
||||
expect(access_token.scopes).to eq(Gitlab::Auth.resource_bot_scopes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user provides scope explicitly' do
|
||||
let(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
|
||||
let_it_be(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
|
||||
|
||||
it 'overrides the default value' do
|
||||
response = subject
|
||||
|
@ -109,7 +109,7 @@ describe Resources::CreateAccessTokenService do
|
|||
end
|
||||
|
||||
context 'when user provides value' do
|
||||
let(:params) { { expires_at: Date.today + 1.month } }
|
||||
let_it_be(:params) { { expires_at: Date.today + 1.month } }
|
||||
|
||||
it 'overrides the default value' do
|
||||
response = subject
|
||||
|
@ -120,7 +120,7 @@ describe Resources::CreateAccessTokenService do
|
|||
end
|
||||
|
||||
context 'when invalid scope is passed' do
|
||||
let(:params) { { scopes: [:invalid_scope] } }
|
||||
let_it_be(:params) { { scopes: [:invalid_scope] } }
|
||||
|
||||
it 'returns error' do
|
||||
response = subject
|
||||
|
@ -145,14 +145,14 @@ describe Resources::CreateAccessTokenService do
|
|||
end
|
||||
|
||||
context 'when resource is a project' do
|
||||
let(:resource_type) { 'project' }
|
||||
let(:resource) { project }
|
||||
let_it_be(:resource_type) { 'project' }
|
||||
let_it_be(:resource) { project }
|
||||
|
||||
it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
|
||||
it_behaves_like 'fails when flag is disabled'
|
||||
|
||||
context 'user with valid permission' do
|
||||
before do
|
||||
before_all do
|
||||
resource.add_maintainer(user)
|
||||
end
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe ResourceAccessTokens::RevokeService do
|
||||
subject { described_class.new(user, resource, access_token).execute }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:access_token) { create(:personal_access_token, user: resource_bot) }
|
||||
|
||||
describe '#execute' do
|
||||
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
|
||||
shared_examples 'revokes access token' do
|
||||
it { expect(subject.success?).to be true }
|
||||
|
||||
it { expect(subject.message).to eq("Revoked access token: #{access_token.name}") }
|
||||
|
||||
it 'revokes token access' do
|
||||
subject
|
||||
|
||||
expect(access_token.reload.revoked?).to be true
|
||||
end
|
||||
|
||||
it 'removes membership of bot user' do
|
||||
subject
|
||||
|
||||
expect(resource.reload.users).not_to include(resource_bot)
|
||||
end
|
||||
|
||||
it 'transfer issuables of bot user to ghost user' do
|
||||
issue = create(:issue, author: resource_bot)
|
||||
|
||||
subject
|
||||
|
||||
expect(issue.reload.author.ghost?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'rollback revoke steps' do
|
||||
it 'does not revoke the access token' do
|
||||
subject
|
||||
|
||||
expect(access_token.reload.revoked?).to be false
|
||||
end
|
||||
|
||||
it 'does not remove bot from member list' do
|
||||
subject
|
||||
|
||||
expect(resource.reload.users).to include(resource_bot)
|
||||
end
|
||||
|
||||
it 'does not transfer issuables of bot user to ghost user' do
|
||||
issue = create(:issue, author: resource_bot)
|
||||
|
||||
subject
|
||||
|
||||
expect(issue.reload.author.ghost?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when resource is a project' do
|
||||
let_it_be(:resource) { create(:project, :private) }
|
||||
let_it_be(:resource_bot) { create(:user, :project_bot) }
|
||||
|
||||
before_all do
|
||||
resource.add_maintainer(user)
|
||||
resource.add_maintainer(resource_bot)
|
||||
end
|
||||
|
||||
it_behaves_like 'revokes access token'
|
||||
|
||||
context 'when revoke fails' do
|
||||
context 'invalid resource type' do
|
||||
subject { described_class.new(user, resource, access_token).execute }
|
||||
|
||||
let_it_be(:resource) { double }
|
||||
let_it_be(:resource_bot) { create(:user, :project_bot) }
|
||||
|
||||
it 'returns error response' do
|
||||
response = subject
|
||||
|
||||
expect(response.success?).to be false
|
||||
expect(response.message).to eq("Failed to find bot user")
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to change(access_token.reload, :revoked) }
|
||||
end
|
||||
|
||||
context 'when migration to ghost user fails' do
|
||||
before do
|
||||
allow_next_instance_of(::Members::DestroyService) do |service|
|
||||
allow(service).to receive(:execute).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rollback revoke steps'
|
||||
end
|
||||
|
||||
context 'when migration to ghost user fails' do
|
||||
before do
|
||||
allow_next_instance_of(::Users::MigrateToGhostUserService) do |service|
|
||||
allow(service).to receive(:execute).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rollback revoke steps'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|