Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-07 15:09:18 +00:00
parent 6d5f18a3c1
commit 40b78ea2b6
35 changed files with 697 additions and 240 deletions

View file

@ -111,7 +111,6 @@ linters:
- Layout/EmptyLineAfterGuardClause
- Layout/LeadingCommentSpace
- Layout/SpaceAfterColon
- Layout/SpaceAfterComma
- Layout/SpaceAroundOperators
- Layout/SpaceBeforeBlockBraces
- Layout/SpaceBeforeComma

View file

@ -750,7 +750,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/support/shared_examples/graphql/mutations/set_multiple_assignees_shared_examples.rb
- ee/spec/support/shared_examples/quick_actions/issue/status_page_quick_actions_shared_examples.rb
- ee/spec/support/shared_examples/services/search_notes_shared_examples.rb
- ee/spec/tasks/gitlab/license_rake_spec.rb
- ee/spec/views/admin/application_settings/_elasticsearch_form.html.haml_spec.rb
- ee/spec/views/groups/_compliance_frameworks.html.haml_spec.rb
- ee/spec/views/groups/edit.html.haml_spec.rb

View file

@ -48,7 +48,7 @@ export default {
}"
>
<gl-link
v-gl-tooltip
v-gl-tooltip:tooltip-container.left
:href="job.status.details_path"
:title="tooltipText"
class="js-job-link gl-display-flex gl-align-items-center"

View file

@ -16,8 +16,8 @@ class Groups::LabelsController < Groups::ApplicationController
format.html do
# at group level we do not want to list project labels,
# we only want `only_group_labels = false` when pulling labels for label filter dropdowns, fetched through json
@labels = available_labels(params.merge(only_group_labels: true)).includes(:group).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord
@labels.each { |label| label.lazy_subscription(current_user) } # preload subscriptions
@labels = available_labels(params.merge(only_group_labels: true)).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord
Preloaders::LabelsPreloader.new(@labels, current_user).preload_all
end
format.json do
render json: LabelSerializer.new.represent_appearance(available_labels)

View file

@ -17,11 +17,14 @@ class Projects::LabelsController < Projects::ApplicationController
feature_category :issue_tracking
def index
@prioritized_labels = @available_labels.prioritized(@project)
@labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format|
format.html
format.html do
@prioritized_labels = @available_labels.prioritized(@project)
@labels = @available_labels.unprioritized(@project).page(params[:page])
# preload group, project, and subscription data
Preloaders::LabelsPreloader.new(@prioritized_labels, current_user, @project).preload_all
Preloaders::LabelsPreloader.new(@labels, current_user, @project).preload_all
end
format.json do
render json: LabelSerializer.new.represent_appearance(@available_labels)
end

View file

@ -5,6 +5,7 @@ module Resolvers
module Users
class SnippetsResolver < BaseResolver
include ResolvesSnippets
include Gitlab::Allowable
alias_method :user, :object
@ -14,6 +15,12 @@ module Resolvers
private
def resolve_snippets(_args)
return Snippet.none unless Ability.allowed?(current_user, :read_user_profile, user)
super
end
def snippet_finder_params(args)
super.merge(author: user)
end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Preloaders
# This class preloads the `project`, `group`, and subscription associations for the given
# labels, user, and project (if provided). A Label can be of type ProjectLabel or GroupLabel
# and the preloader supports both.
#
# Usage:
# labels = Label.where(...)
# Preloaders::LabelsPreloader.new(labels, current_user, @project).preload_all
# labels.first.project # won't fire any query
class LabelsPreloader
attr_reader :labels, :user, :project
def initialize(labels, user, project = nil)
@labels, @user, @project = labels, user, project
end
def preload_all
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(labels.select {|l| l.is_a? ProjectLabel }, { project: [:project_feature, namespace: :route] })
preloader.preload(labels.select {|l| l.is_a? GroupLabel }, { group: :route })
labels.each do |label|
label.lazy_subscription(user)
label.lazy_subscription(user, project) if project.present?
end
end
end
end
Preloaders::LabelsPreloader.prepend_if_ee('EE::Preloaders::LabelsPreloader')

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class SystemHooksService
BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group, ProjectMember].freeze
BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group, ProjectMember, User].freeze
def execute_hooks_for(model, event)
data = build_event_data(model, event)
@ -47,15 +47,6 @@ class SystemHooksService
if event == :rename || event == :transfer
data[:old_path_with_namespace] = model.old_path_with_namespace
end
when User
data.merge!(user_data(model))
case event
when :rename
data[:old_username] = model.username_before_last_save
when :failed_login
data[:state] = model.state
end
end
data
@ -79,15 +70,6 @@ class SystemHooksService
}
end
def user_data(model)
{
name: model.name,
email: model.email,
user_id: model.id,
username: model.username
}
end
def builder_driven_event_data_available?(model)
model.class.in?(BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES)
end
@ -100,10 +82,10 @@ class SystemHooksService
Gitlab::HookData::GroupBuilder
when ProjectMember
Gitlab::HookData::ProjectMemberBuilder
when User
Gitlab::HookData::UserBuilder
end
builder_class.new(model).build(event)
end
end
SystemHooksService.prepend_if_ee('EE::SystemHooksService')

View file

@ -5,4 +5,4 @@
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default'
.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json),'data-project-branch': current_ref }
.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json), 'data-project-branch': current_ref }

View file

@ -24,7 +24,7 @@
%h5 Request headers:
%pre
- hook_log.request_headers.each do |k,v|
- hook_log.request_headers.each do |k, v|
<strong>#{k}:</strong> #{v}
%br
@ -34,7 +34,7 @@
#{Gitlab::Json.pretty_generate(hook_log.request_data)}
%h5 Response headers:
%pre
- hook_log.response_headers.each do |k,v|
- hook_log.response_headers.each do |k, v|
<strong>#{k}:</strong> #{v}
%br

View file

@ -0,0 +1,5 @@
---
title: Reduce queries on projects labels controller
merge_request: 57864
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Drop unused mirror_data index
merge_request: 58349
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Avoid listing snippets through GraphQL when user profile is private
merge_request: 58739
author:
type: fixed

View file

@ -0,0 +1,8 @@
---
name: cached_sidebar_open_epics_count
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58064
rollout_issue_url:
milestone: '13.11'
type: development
group: group::product planning
default_enabled: true

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveIndexMirrorDataOnNextExecutionAndRetryCount < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
INDEX_NAME = 'index_mirror_data_on_next_execution_and_retry_count'
def up
remove_concurrent_index(
:project_mirror_data,
%i[next_execution_timestamp retry_count],
name: INDEX_NAME
)
end
def down
add_concurrent_index(
:project_mirror_data,
%i[next_execution_timestamp retry_count],
name: INDEX_NAME
)
end
end

View file

@ -0,0 +1 @@
3420d83bf8a1f44e69960849efa25525883f17a2776ae3ce28db855cd550ca8e

View file

@ -23140,8 +23140,6 @@ CREATE INDEX index_milestones_on_title_trigram ON milestones USING gin (title gi
CREATE INDEX index_mirror_data_non_scheduled_or_started ON project_mirror_data USING btree (next_execution_timestamp, retry_count) WHERE ((status)::text <> ALL ('{scheduled,started}'::text[]));
CREATE INDEX index_mirror_data_on_next_execution_and_retry_count ON project_mirror_data USING btree (next_execution_timestamp, retry_count);
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE (completed_at IS NULL);

View file

@ -772,113 +772,7 @@ argument :title, GraphQL::STRING_TYPE,
## Authorization
Authorizations can be applied to both types and fields using the same
abilities as in the Rails app.
If the:
- Currently authenticated user fails the authorization, the authorized
resource is returned as `null`.
- Resource is part of a collection, the collection is filtered to
exclude the objects that the user's authorization checks failed against.
Also see [authorizing resources in a mutation](#authorizing-resources).
NOTE:
Try to load only what the currently authenticated user is allowed to
view with our existing finders first, without relying on authorization
to filter the records. This minimizes database queries and unnecessary
authorization checks of the loaded records.
### Type authorization
Authorize a type by passing an ability to the `authorize` method. All
fields with the same type is authorized by checking that the
currently authenticated user has the required ability.
For example, the following authorization ensures that the currently
authenticated user can only see projects that they have the
`read_project` ability for (so long as the project is returned in a
field that uses `Types::ProjectType`):
```ruby
module Types
class ProjectType < BaseObject
authorize :read_project
end
end
```
You can also authorize against multiple abilities, in which case all of
the ability checks must pass.
For example, the following authorization ensures that the currently
authenticated user must have `read_project` and `another_ability`
abilities to see a project:
```ruby
module Types
class ProjectType < BaseObject
authorize [:read_project, :another_ability]
end
end
```
### Field authorization
Fields can be authorized with the `authorize` option.
For example, the following authorization ensures that the currently
authenticated user must have the `owner_access` ability to see the
project:
```ruby
module Types
class MyType < BaseObject
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access
end
end
```
Fields can also be authorized against multiple abilities, in which case
all of ability checks must pass. This requires explicitly
passing a block to `field`:
```ruby
module Types
class MyType < BaseObject
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
authorize [:owner_access, :another_ability]
end
end
end
```
If the field's type already [has a particular
authorization](#type-authorization) then there is no need to add that
same authorization to the field.
### Type and Field authorizations together
Authorizations are cumulative, so where authorizations are defined on
a field, and also on the field's type, then the currently authenticated
user would need to pass all ability checks.
In the following simplified example the currently authenticated user
would need both `first_permission` and `second_permission` abilities in
order to see the author of the issue.
```ruby
class UserType
authorize :first_permission
end
```
```ruby
class IssueType
field :author, UserType, authorize: :second_permission
end
```
See: [GraphQL Authorization](graphql_guide/authorization.md)
## Resolvers

View file

@ -142,6 +142,14 @@ migration performing the scheduling. Otherwise the background migration would be
scheduled multiple times on systems that are upgrading multiple patch releases at
once.
When you start the second post-deployment migration, you should delete any
previously queued jobs from the initial migration with the provided
helper:
```ruby
delete_queued_jobs('BackgroundMigrationClassName')
```
## Cleaning Up
NOTE:

View file

@ -0,0 +1,223 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GraphQL Authorization
Authorizations can be applied in these places:
- Types:
- Objects (all classes descending from `::Types::BaseObject`)
- Enums (all classes descending from `::Types::BaseEnum`)
- Resolvers:
- Field resolvers (all classes descending from `::Types::BaseResolver`)
- Mutations (all classes descending from `::Types::BaseMutation`)
- Fields (all fields declared using the `field` DSL method)
Authorizations cannot be specified for abstract types (interfaces and
unions). Abstract types delegate to their member types.
Basic built in scalars (such as integers) do not have authorizations.
Our authorization system uses the same [`DeclarativePolicy`](../policies.md)
system as throughout the rest of the application.
- For single values (such as `Query.project`), if the currently authenticated
user fails the authorization, the field resolves to `null`.
- For collections (such as `Project.issues`), the collection is filtered to
exclude the objects that the user's authorization checks failed against. This
process of filtering (also known as _redaction_) happens after pagination, so
some pages may be smaller than the requested page size, due to redacted
objects being removed.
Also see [authorizing resources in a mutation](../api_graphql_styleguide.md#authorizing-resources).
NOTE:
The best practice is to load only what the currently authenticated user is allowed to
view with our existing finders first, without relying on authorization
to filter the records. This minimizes database queries and unnecessary
authorization checks of the loaded records. It also avoids situations,
such as short pages, which can expose the presence of confidential resources.
See [`authorization_spec.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/graphql/features/authorization_spec.rb)
for examples of all the authorization schemes discussed here.
## Type authorization
Authorize a type by passing an ability to the `authorize` method. All
fields with the same type is authorized by checking that the
currently authenticated user has the required ability.
For example, the following authorization ensures that the currently
authenticated user can only see projects that they have the
`read_project` ability for (so long as the project is returned in a
field that uses `Types::ProjectType`):
```ruby
module Types
class ProjectType < BaseObject
authorize :read_project
end
end
```
You can also authorize against multiple abilities, in which case all of
the ability checks must pass.
For example, the following authorization ensures that the currently
authenticated user must have `read_project` and `another_ability`
abilities to see a project:
```ruby
module Types
class ProjectType < BaseObject
authorize [:read_project, :another_ability]
end
end
```
## Resolver authorization
Resolvers can have their own authorizations, which can be applied either to the
parent object or to the resolved values.
An example of a resolver that authorizes against the parent is
`Resolvers::BoardListsResolver`, which requires that the parent
satisfy `:read_list` before it runs.
An example which authorizes against the resolved resource is
`Resolvers::Ci::ConfigResolver`, which requires that the resolved value satisfy
`:read_pipeline`.
To authorize against the parent, the resolver must _opt in_ (because this
was not the default value initially), by declaring this with `authorizes_object!`:
```ruby
module Resolvers
class MyResolver < BaseResolver
authorizes_object!
authorize :some_permission
end
end
```
To authorize against the resolved value, the resolver must apply the
authorization at some point, typically by using `#authorized_find!(**args)`:
```ruby
module Resolvers
class MyResolver < BaseResolver
authorize :some_permission
def resolve(**args)
authorized_find!(**args) # calls find_object
end
def find_object(id:)
MyThing.find(id)
end
end
end
```
Of the two approaches, authorizing the object is more efficient, because it
helps avoid unnecessary queries.
## Field authorization
Fields can be authorized with the `authorize` option.
Fields authorization is checked against the current object, and
authorization happens _before_ resolution, which means that
fields do not have access to the resolved resource. If you need to
apply an authorization check to a field, you probably want to add
authorization to the resolver, or ideally to the type.
For example, the following authorization ensures that the
authenticated user must have administrator level access to the project
to view the `secretName` field:
```ruby
module Types
class ProjectType < BaseObject
field :secret_name, ::GraphQL::STRING_TYPE, null: true, authorize: :owner_access
end
end
```
In this example, we use field authorization (such as
`Ability.allowed?(current_user, :read_transactions, bank_account)`) to avoid
a more expensive query:
```ruby
module Types
class BankAccountType < BaseObject
field :transactions, ::Types::TransactionType.connection_type, null: true,
authorize: :read_transactions
end
end
```
Field authorization is recommended for:
- Scalar fields (strings, booleans, or numbers) that should have different levels
of access controls to other fields.
- Object and collection fields where an access check can be applied to the
parent to save the field resolution, and avoid individual policy checks
on each resolved object.
Field authorization does not replace object level checks, unless the object
precisely matches the access level of the parent project. For example, issues
can be confidential, independent of the access level of the parent. Therefore,
we should not use field authorization for `Project.issue`.
You can also authorize fields against multiple abilities. Pass the abilities
as an array instead of as a single value:
```ruby
module Types
class MyType < BaseObject
field :hidden_field, ::GraphQL::INT_TYPE,
null: true,
authorize: [:owner_access, :another_ability]
end
end
```
The field authorization on `MyType.hiddenField` implies the following tests:
```ruby
Ability.allowed?(current_user, :owner_access, object_of_my_type) &&
Ability.allowed?(current_user, :another_ability, object_of_my_type)
```
## Type and Field authorizations together
Authorizations are cumulative. In other words, the currently authenticated
user may need to pass authorization requirements on both a field and a field's
type.
In the following simplified example the currently authenticated user
needs both `first_permission` on the user and `second_permission` on the
issue to see the author of the issue.
```ruby
class UserType
authorize :first_permission
end
```
```ruby
class IssueType
field :author, UserType, authorize: :second_permission
end
```
The combination of the object authorization on `UserType` and the field authorization on `IssueType.author` implies the following tests:
```ruby
Ability.allowed?(current_user, :second_permission, issue) &&
Ability.allowed?(current_user, :first_permission, issue.author)
```

View file

@ -17,6 +17,7 @@ feedback, and suggestions.
- [GraphQL API documentation style guide](../documentation/graphql_styleguide.md): documentation
style guide for GraphQL.
- [GraphQL API](../../api/graphql/index.md): user documentation for the GitLab GraphQL API.
- [GraphQL authorization](authorization.md): guide to using authorization in GraphQL.
- [GraphQL BatchLoader](batchloader.md): development documentation on the BatchLoader.
- [GraphQL pagination](pagination.md): development documentation on pagination.
- [GraphQL Pro](graphql_pro.md): information on our GraphQL Pro subscription.

View file

@ -497,6 +497,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
redis_slot: compliance
expiry: 42 # 6 weeks
aggregation: weekly
feature_flag: usage_data_i_compliance_credential_inventory
```
Keys:
@ -528,7 +529,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
aggregation.
- `aggregation`: may be set to a `:daily` or `:weekly` key. Defines how counting data is stored in Redis.
Aggregation on a `daily` basis does not pull more fine grained data.
- `feature_flag`: optional `default_enabled: :yaml`. If no feature flag is set then the tracking is enabled. For details, see our [GitLab internal Feature flags](../feature_flags/index.md) documentation. The feature flags are owned by the group adding the event tracking.
- `feature_flag`: optional `default_enabled: :yaml`. If no feature flag is set then the tracking is enabled. One feature flag can be used for multiple events. For details, see our [GitLab internal Feature flags](../feature_flags/index.md) documentation. The feature flags are owned by the group adding the event tracking.
Use one of the following methods to track events:
@ -567,8 +568,6 @@ Use one of the following methods to track events:
1. Track event in API using `increment_unique_values(event_name, values)` helper method.
To be able to track the event, Usage Ping must be enabled and the event feature `usage_data_<event_name>` must be enabled.
Arguments:
- `event_name`: event name.
@ -612,10 +611,6 @@ Use one of the following methods to track events:
API requests are protected by checking for a valid CSRF token.
To increment the values, the related feature `usage_data_<event_name>` should be
set to `default_enabled: true`. For more information, see
[Feature flags in development of GitLab](../feature_flags/index.md).
```plaintext
POST /usage_data/increment_unique_users
```
@ -640,8 +635,6 @@ Use one of the following methods to track events:
Usage Data API is behind `usage_data_api` feature flag which, as of GitLab 13.7, is
now set to `default_enabled: true`.
Each event tracked using Usage Data API is behind a feature flag `usage_data_#{event_name}` which should be `default_enabled: true`
```javascript
import api from '~/api';

View file

@ -323,3 +323,36 @@ To remove a child epic from a parent epic:
1. Select the <kbd>x</kbd> button in the parent epic's list of epics.
1. Select **Remove** in the **Remove epic** warning message.
## Cached epic count
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299540) in GitLab 13.11.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-cached-epic-count).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
In a group, the sidebar displays the total count of open epics and this value is cached if higher
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
### Enable or disable cached epic count **(PREMIUM SELF)**
Cached epic count in the left sidebar is under development but ready for production use. It is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can disable it.
To disable it:
```ruby
Feature.disable(:cached_sidebar_open_epics_count)
```
To enable it:
```ruby
Feature.enable(:cached_sidebar_open_epics_count)
```

View file

@ -22,13 +22,15 @@ module API
users: Entities::UserBasic
}.freeze
SCOPE_PRELOAD_METHOD = {
merge_requests: :with_api_entity_associations,
projects: :with_api_entity_associations,
issues: :with_api_entity_associations,
milestones: :with_api_entity_associations,
commits: :with_api_commit_entity_associations
}.freeze
def scope_preload_method
{
merge_requests: :with_api_entity_associations,
projects: :with_api_entity_associations,
issues: :with_api_entity_associations,
milestones: :with_api_entity_associations,
commits: :with_api_commit_entity_associations
}.freeze
end
def search(additional_params = {})
search_params = {
@ -60,7 +62,7 @@ module API
end
def preload_method
SCOPE_PRELOAD_METHOD[params[:scope].to_sym]
scope_preload_method[params[:scope].to_sym]
end
def verify_search_scope!(resource:)

View file

@ -236,6 +236,14 @@ module Gitlab
Gitlab::ApplicationContext.with_context(caller_id: self.class.to_s, &block)
end
def delete_queued_jobs(class_name)
Gitlab::BackgroundMigration.steal(class_name) do |job|
job.delete
false
end
end
private
def track_in_database(class_name, arguments)

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
module Gitlab
module HookData
class UserBuilder < BaseBuilder
alias_method :user, :object
# Sample data
# {
# :created_at=>"2021-04-02T10:00:26Z",
# :updated_at=>"2021-04-02T10:00:26Z",
# :event_name=>"user_create",
# :name=>"John Doe",
# :email=>"john@example.com",
# :user_id=>1,
# :username=>"johndoe"
# }
def build(event)
[
timestamps_data,
event_data(event),
user_data,
event_specific_user_data(event)
].reduce(:merge)
end
private
def user_data
{
name: user.name,
email: user.email,
user_id: user.id,
username: user.username
}
end
def event_specific_user_data(event)
case event
when :rename
{ old_username: user.username_before_last_save }
when :failed_login
{ state: user.state }
else
{}
end
end
end
end
end
Gitlab::HookData::UserBuilder.prepend_if_ee('EE::Gitlab::HookData::UserBuilder')

View file

@ -60,7 +60,7 @@ RSpec.describe Groups::LabelsController do
create_list(:group_label, 3, group: group)
# some n+1 queries still exist
expect { get :index, params: { group_id: group.to_param } }.not_to exceed_all_query_limit(control.count).with_threshold(12)
expect { get :index, params: { group_id: group.to_param } }.not_to exceed_all_query_limit(control.count).with_threshold(10)
expect(assigns(:labels).count).to eq(4)
end
end

View file

@ -93,6 +93,26 @@ RSpec.describe Projects::LabelsController do
end
end
context 'with views rendered' do
render_views
before do
list_labels
end
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_labels }
create_list(:label, 3, project: project)
create_list(:group_label, 3, group: group)
# some n+1 queries still exist
# calls to get max project authorization access level
expect { list_labels }.not_to exceed_all_query_limit(control.count).with_threshold(25)
expect(assigns(:labels).count).to eq(10)
end
end
def list_labels
get :index, params: { namespace_id: project.namespace.to_param, project_id: project }
end

View file

@ -75,9 +75,19 @@ RSpec.describe Resolvers::Users::SnippetsResolver do
end.to raise_error(GraphQL::CoercionError)
end
end
context 'when user profile is private' do
it 'does not return snippets for that user' do
expect(resolve_snippets(obj: other_user)).to contain_exactly(other_personal_snippet, other_project_snippet)
other_user.update!(private_profile: true)
expect(resolve_snippets(obj: other_user)).to be_empty
end
end
end
def resolve_snippets(args: {})
resolve(described_class, args: args, ctx: { current_user: current_user }, obj: current_user)
def resolve_snippets(args: {}, context_user: current_user, obj: current_user)
resolve(described_class, args: args, ctx: { current_user: context_user }, obj: obj)
end
end

View file

@ -515,64 +515,18 @@ RSpec.describe GroupsHelper do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'group') }
subject { helper.cached_issuables_count(group, type: type) }
before do
allow(helper).to receive(:current_user) { current_user }
allow(count_service).to receive(:new).and_call_original
end
shared_examples 'caching issuables count' do
it 'calls the correct service class' do
subject
expect(count_service).to have_received(:new).with(group, current_user)
end
it 'returns all digits for count value under 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(999)
end
expect(subject).to eq('999')
end
it 'returns truncated digits for count value over 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(2300)
end
expect(subject).to eq('2.3k')
end
it 'returns truncated digits for count value over 10000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(12560)
end
expect(subject).to eq('12.6k')
end
it 'returns truncated digits for count value over 100000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(112560)
end
expect(subject).to eq('112.6k')
end
end
context 'with issue type' do
context 'with issues type' do
let(:type) { :issues }
let(:count_service) { Groups::OpenIssuesCountService }
it_behaves_like 'caching issuables count'
it_behaves_like 'cached issuables count'
end
context 'with merge request type' do
context 'with merge requests type' do
let(:type) { :merge_requests }
let(:count_service) { Groups::MergeRequestsCountService }
it_behaves_like 'caching issuables count'
it_behaves_like 'cached issuables count'
end
end
end

View file

@ -431,4 +431,21 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
model.bulk_migrate_in(10.minutes, [%w(Class hello world)])
end
end
describe '#delete_queued_jobs' do
let(:job1) { double }
let(:job2) { double }
it 'deletes all queued jobs for the given background migration' do
expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackgroundMigrationClassName') do |&block|
expect(block.call(job1)).to be(false)
expect(block.call(job2)).to be(false)
end
expect(job1).to receive(:delete)
expect(job2).to receive(:delete)
model.delete_queued_jobs('BackgroundMigrationClassName')
end
end
end

View file

@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::HookData::UserBuilder do
let_it_be(:user) { create(:user, name: 'John Doe', username: 'johndoe', email: 'john@example.com') }
describe '#build' do
let(:data) { described_class.new(user).build(event) }
let(:event_name) { data[:event_name] }
let(:attributes) do
[
:event_name, :created_at, :updated_at, :name, :email, :user_id, :username
]
end
context 'data' do
shared_examples_for 'includes the required attributes' do
it 'includes the required attributes' do
expect(data).to include(*attributes)
expect(data[:name]).to eq('John Doe')
expect(data[:email]).to eq('john@example.com')
expect(data[:user_id]).to eq(user.id)
expect(data[:username]).to eq('johndoe')
expect(data[:created_at]).to eq(user.created_at.xmlschema)
expect(data[:updated_at]).to eq(user.updated_at.xmlschema)
end
end
shared_examples_for 'does not include old username attributes' do
it 'does not include old username attributes' do
expect(data).not_to include(:old_username)
end
end
shared_examples_for 'does not include state attributes' do
it 'does not include state attributes' do
expect(data).not_to include(:state)
end
end
context 'on create' do
let(:event) { :create }
it { expect(event_name).to eq('user_create') }
it_behaves_like 'includes the required attributes'
it_behaves_like 'does not include old username attributes'
it_behaves_like 'does not include state attributes'
end
context 'on destroy' do
let(:event) { :destroy }
it { expect(event_name).to eq('user_destroy') }
it_behaves_like 'includes the required attributes'
it_behaves_like 'does not include old username attributes'
it_behaves_like 'does not include state attributes'
end
context 'on rename' do
let(:event) { :rename }
it { expect(event_name).to eq('user_rename') }
it_behaves_like 'includes the required attributes'
it_behaves_like 'does not include state attributes'
it 'includes old username details' do
allow(user).to receive(:username_before_last_save).and_return('old-username')
expect(data[:old_username]).to eq(user.username_before_last_save)
end
end
context 'on failed_login' do
let(:event) { :failed_login }
it { expect(event_name).to eq('user_failed_login') }
it_behaves_like 'includes the required attributes'
it_behaves_like 'does not include old username attributes'
it 'includes state details' do
user.ldap_block!
expect(data[:state]).to eq('ldap_blocked')
end
end
end
end
end

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Preloaders::LabelsPreloader do
let_it_be(:user) { create(:user) }
shared_examples 'an efficient database query' do
let(:subscriptions) { labels.each { |l| create(:subscription, subscribable: l, project: l.project, user: user) }}
it 'does not make n+1 queries' do
first_label = labels_with_preloaded_data.first
clean_labels = labels_with_preloaded_data
expect { access_data(clean_labels) }.to issue_same_number_of_queries_as { access_data([first_label]) }
end
end
context 'project labels' do
let_it_be(:projects) { create_list(:project, 3, :public, :repository) }
let_it_be(:labels) { projects.each { |p| create(:label, project: p) } }
it_behaves_like 'an efficient database query'
end
context 'group labels' do
let_it_be(:groups) { create_list(:group, 3) }
let_it_be(:labels) { groups.each { |g| create(:group_label, group: g) } }
it_behaves_like 'an efficient database query'
end
private
def labels_with_preloaded_data
l = Label.where(id: labels.map(&:id))
described_class.new(l, user).preload_all
l
end
def access_data(labels)
labels.each do |label|
if label.is_a?(ProjectLabel)
label.project.project_feature
label.lazy_subscription(user, label.project)
elsif label.is_a?(GroupLabel)
label.group.route
label.lazy_subscription(user)
end
end
end
end

View file

@ -113,37 +113,9 @@ RSpec.describe SystemHooksService do
expect(data[:old_path]).to eq('old-path')
end
end
context 'user_rename' do
it 'contains old and new username' do
allow(user).to receive(:username_before_last_save).and_return('old-username')
data = event_data(user, :rename)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
expect(data[:username]).to eq(user.username)
expect(data[:old_username]).to eq(user.username_before_last_save)
end
end
context 'user_failed_login' do
it 'contains state of user' do
user.ldap_block!
data = event_data(user, :failed_login)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :state)
expect(data[:username]).to eq(user.username)
expect(data[:state]).to eq('ldap_blocked')
end
end
end
context 'event names' do
it { expect(event_name(user, :create)).to eq "user_create" }
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
it { expect(event_name(user, :rename)).to eq 'user_rename' }
it { expect(event_name(user, :failed_login)).to eq 'user_failed_login' }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
it { expect(event_name(project, :rename)).to eq "project_rename" }

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
# This shared_example requires the following variables:
# - current_user
# - group
# - type, the issuable type (ie :issues, :merge_requests)
# - count_service, the Service used by the specified issuable type
RSpec.shared_examples 'cached issuables count' do
subject { helper.cached_issuables_count(group, type: type) }
before do
allow(helper).to receive(:current_user) { current_user }
allow(count_service).to receive(:new).and_call_original
end
it 'calls the correct service class' do
subject
expect(count_service).to have_received(:new).with(group, current_user)
end
it 'returns all digits for count value under 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(999)
end
expect(subject).to eq('999')
end
it 'returns truncated digits for count value over 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(2300)
end
expect(subject).to eq('2.3k')
end
it 'returns truncated digits for count value over 10000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(12560)
end
expect(subject).to eq('12.6k')
end
it 'returns truncated digits for count value over 100000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(112560)
end
expect(subject).to eq('112.6k')
end
end