Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-05 21:09:42 +00:00
parent 38ceebb9b3
commit 53288eeb63
76 changed files with 1076 additions and 133 deletions

View File

@ -89,7 +89,7 @@ downtime_check:
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- junit_rspec.xml
- log/*.log
reports:
junit: junit_rspec.xml

View File

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

View File

@ -0,0 +1,2 @@
// /dag is an alias for show
import '../show/index';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Increase constrast ratio of text in some tables
merge_request: 30903
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Support limits for offset based pagination
merge_request: 28460
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fixed cancel reply button not alerting the user
merge_request:
author:
type: fixed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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