Commit 7335c312 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'ajk-add-user-mr-interaction' into 'master'

Add user-merge-request interaction type [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!54588
parents ab8cdbe8 774db6a8
...@@ -27,6 +27,15 @@ module Types ...@@ -27,6 +27,15 @@ module Types
end end
end end
# Helper to define an enum member for each element of a Rails AR enum
def from_rails_enum(enum, description:)
enum.each_key do |name|
value name.to_s.upcase,
value: name,
description: format(description, name: name)
end
end
def value(*args, **kwargs, &block) def value(*args, **kwargs, &block)
enum[args[0].downcase] = kwargs[:value] || args[0] enum[args[0].downcase] = kwargs[:value] || args[0]
gitlab_deprecation(kwargs) gitlab_deprecation(kwargs)
......
# frozen_string_literal: true
module FindClosest
# Find the closest node of a given type above this node, and return the domain object
def closest_parent(type, parent)
parent = parent.try(:parent) while parent && parent.object.class != type
return unless parent
parent.object.object
end
end
...@@ -23,7 +23,7 @@ module Types ...@@ -23,7 +23,7 @@ module Types
A global identifier. A global identifier.
A global identifier represents an object uniquely across the application. A global identifier represents an object uniquely across the application.
An example of such an identifier is "gid://gitlab/User/1". An example of such an identifier is `"gid://gitlab/User/1"`.
Global identifiers are encoded as strings. Global identifiers are encoded as strings.
DESC DESC
......
# frozen_string_literal: true
module Types
class MergeRequestReviewStateEnum < BaseEnum
graphql_name 'MergeRequestReviewState'
description 'State of a review of a GitLab merge request.'
from_rails_enum(::MergeRequestReviewer.states,
description: "The merge request is %{name}.")
end
end
...@@ -132,7 +132,10 @@ module Types ...@@ -132,7 +132,10 @@ module Types
description: 'The milestone of the merge request.' description: 'The milestone of the merge request.'
field :assignees, Types::UserType.connection_type, null: true, complexity: 5, field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request.' description: 'Assignees of the merge request.'
field :reviewers, Types::UserType.connection_type, null: true, complexity: 5, field :reviewers,
type: Types::MergeRequests::ReviewerType.connection_type,
null: true,
complexity: 5,
description: 'Users from whom a review has been requested.' description: 'Users from whom a review has been requested.'
field :author, Types::UserType, null: true, field :author, Types::UserType, null: true,
description: 'User who created this merge request.' description: 'User who created this merge request.'
......
# frozen_string_literal: true
module Types
module MergeRequests
class ReviewerType < ::Types::UserType
include FindClosest
graphql_name 'MergeRequestReviewer'
description 'A user from whom a merge request review has been requested.'
authorize :read_user
field :merge_request_interaction,
type: ::Types::UserMergeRequestInteractionType,
null: true,
extras: [:parent],
description: "Details of this user's interactions with the merge request."
def merge_request_interaction(parent:)
merge_request = closest_parent(::Types::MergeRequestType, parent)
return unless merge_request
Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
end
end
end
end
# frozen_string_literal: true
module Types
class UserMergeRequestInteractionType < BaseObject
graphql_name 'UserMergeRequestInteraction'
description <<~MD
Information about a merge request given a specific user.
This object has two parts to its state: a `User` and a `MergeRequest`. All
fields relate to interactions between the two entities.
MD
authorize :read_merge_request
field :can_merge,
type: ::GraphQL::BOOLEAN_TYPE,
null: false,
calls_gitaly: true,
method: :can_merge?,
description: 'Whether this user can merge this merge request.'
field :can_update,
type: ::GraphQL::BOOLEAN_TYPE,
null: false,
method: :can_update?,
description: 'Whether this user can update this merge request.'
field :review_state,
::Types::MergeRequestReviewStateEnum,
null: true,
description: 'The state of the review by this user.'
field :reviewed,
type: ::GraphQL::BOOLEAN_TYPE,
null: false,
method: :reviewed?,
description: 'Whether this user has provided a review for this merge request.'
field :approved,
type: ::GraphQL::BOOLEAN_TYPE,
null: false,
method: :approved?,
description: 'Whether this user has approved this merge request.'
end
end
::Types::UserMergeRequestInteractionType.prepend_if_ee('EE::Types::UserMergeRequestInteractionType')
# frozen_string_literal: true
module Users
class MergeRequestInteraction
def initialize(user:, merge_request:)
@user = user
@merge_request = merge_request
end
def declarative_policy_subject
merge_request
end
def can_merge?
merge_request.can_be_merged_by?(user)
end
def can_update?
user.can?(:update_merge_request, merge_request)
end
def review_state
reviewer&.state
end
def reviewed?
reviewer&.reviewed? == true
end
def approved?
merge_request.approvals.any? { |app| app.user_id == user.id }
end
private
def reviewer
@reviewer ||= merge_request.merge_request_reviewers.find { |r| r.user_id == user.id }
end
attr_reader :user, :merge_request
end
end
::Users::MergeRequestInteraction.prepend_if_ee('EE::Users::MergeRequestInteraction')
...@@ -13,7 +13,11 @@ class UserPresenter < Gitlab::View::Presenter::Delegated ...@@ -13,7 +13,11 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
private private
def can?(*args)
user.can?(*args)
end
def should_be_private? def should_be_private?
!can?(current_user, :read_user_profile, user) !Ability.allowed?(current_user, :read_user_profile, user)
end end
end end
---
title: Add user-merge request interaction type
merge_request: 54588
author:
type: added
...@@ -692,6 +692,16 @@ An API Fuzzing scan profile. ...@@ -692,6 +692,16 @@ An API Fuzzing scan profile.
| `name` | [`String`](#string) | The unique name of the profile. | | `name` | [`String`](#string) | The unique name of the profile. |
| `yaml` | [`String`](#string) | A syntax highlit HTML representation of the YAML. | | `yaml` | [`String`](#string) | A syntax highlit HTML representation of the YAML. |
### `ApprovalRule`
Describes a rule for who can approve merge requests.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `id` | [`GlobalID!`](#globalid) | ID of the rule. |
| `name` | [`String`](#string) | Name of the rule. |
| `type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. |
### `AwardEmoji` ### `AwardEmoji`
An emoji awarded by a user. An emoji awarded by a user.
...@@ -3916,7 +3926,7 @@ An edge in a connection. ...@@ -3916,7 +3926,7 @@ An edge in a connection.
| `rebaseCommitSha` | [`String`](#string) | Rebase commit SHA of the merge request. | | `rebaseCommitSha` | [`String`](#string) | Rebase commit SHA of the merge request. |
| `rebaseInProgress` | [`Boolean!`](#boolean) | Indicates if there is a rebase currently in progress for the merge request. | | `rebaseInProgress` | [`Boolean!`](#boolean) | Indicates if there is a rebase currently in progress for the merge request. |
| `reference` | [`String!`](#string) | Internal reference of the merge request. Returned in shortened format by default. | | `reference` | [`String!`](#string) | Internal reference of the merge request. Returned in shortened format by default. |
| `reviewers` | [`UserConnection`](#userconnection) | Users from whom a review has been requested. | | `reviewers` | [`MergeRequestReviewerConnection`](#mergerequestreviewerconnection) | Users from whom a review has been requested. |
| `securityAutoFix` | [`Boolean`](#boolean) | Indicates if the merge request is created by @GitLab-Security-Bot. | | `securityAutoFix` | [`Boolean`](#boolean) | Indicates if the merge request is created by @GitLab-Security-Bot. |
| `securityReportsUpToDateOnTargetBranch` | [`Boolean!`](#boolean) | Indicates if the target branch security reports are out of date. | | `securityReportsUpToDateOnTargetBranch` | [`Boolean!`](#boolean) | Indicates if the target branch security reports are out of date. |
| `shouldBeRebased` | [`Boolean!`](#boolean) | Indicates if the merge request will be rebased. | | `shouldBeRebased` | [`Boolean!`](#boolean) | Indicates if the merge request will be rebased. |
...@@ -4038,6 +4048,56 @@ Check permissions for the current user on a merge request. ...@@ -4038,6 +4048,56 @@ Check permissions for the current user on a merge request.
| `revertOnCurrentMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `revert_on_current_merge_request` on this resource. | | `revertOnCurrentMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `revert_on_current_merge_request` on this resource. |
| `updateMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `update_merge_request` on this resource. | | `updateMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `update_merge_request` on this resource. |
### `MergeRequestReviewer`
A user from whom a merge request review has been requested.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `assignedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests assigned to the user. |
| `authoredMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests authored by the user. |
| `avatarUrl` | [`String`](#string) | URL of the user's avatar. |
| `bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. |
| `callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. |
| `email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: `User.publicEmail`. |
| `groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| `groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. |
| `id` | [`ID!`](#id) | ID of the user. |
| `location` | [`String`](#string) | The location of the user. |
| `mergeRequestInteraction` | [`UserMergeRequestInteraction`](#usermergerequestinteraction) | Details of this user's interactions with the merge request. |
| `name` | [`String!`](#string) | Human-readable name of the user. |
| `projectMemberships` | [`ProjectMemberConnection`](#projectmemberconnection) | Project memberships of the user. |
| `publicEmail` | [`String`](#string) | User's public email. |
| `reviewRequestedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests assigned to the user for review. |
| `snippets` | [`SnippetConnection`](#snippetconnection) | Snippets authored by the user. |
| `starredProjects` | [`ProjectConnection`](#projectconnection) | Projects starred by the user. |
| `state` | [`UserState!`](#userstate) | State of the user. |
| `status` | [`UserStatus`](#userstatus) | User status. |
| `todos` | [`TodoConnection`](#todoconnection) | To-do items of the user. |
| `userPermissions` | [`UserPermissions!`](#userpermissions) | Permissions for the current user on the resource. |
| `username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| `webPath` | [`String!`](#string) | Web path of the user. |
| `webUrl` | [`String!`](#string) | Web URL of the user. |
### `MergeRequestReviewerConnection`
The connection type for MergeRequestReviewer.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `edges` | [`[MergeRequestReviewerEdge]`](#mergerequestrevieweredge) | A list of edges. |
| `nodes` | [`[MergeRequestReviewer]`](#mergerequestreviewer) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
### `MergeRequestReviewerEdge`
An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`MergeRequestReviewer`](#mergerequestreviewer) | The item at the end of the edge. |
### `MergeRequestReviewerRereviewPayload` ### `MergeRequestReviewerRereviewPayload`
Autogenerated return type of MergeRequestReviewerRereview. Autogenerated return type of MergeRequestReviewerRereview.
...@@ -6623,6 +6683,22 @@ An edge in a connection. ...@@ -6623,6 +6683,22 @@ An edge in a connection.
| `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`User`](#user) | The item at the end of the edge. | | `node` | [`User`](#user) | The item at the end of the edge. |
### `UserMergeRequestInteraction`
Information about a merge request given a specific user.
This object has two parts to its state: a `User` and a `MergeRequest`. All
fields relate to interactions between the two entities.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `applicableApprovalRules` | [`[ApprovalRule!]`](#approvalrule) | Approval rules that apply to this user for this merge request. |
| `approved` | [`Boolean!`](#boolean) | Whether this user has approved this merge request. |
| `canMerge` | [`Boolean!`](#boolean) | Whether this user can merge this merge request. |
| `canUpdate` | [`Boolean!`](#boolean) | Whether this user can update this merge request. |
| `reviewState` | [`MergeRequestReviewState`](#mergerequestreviewstate) | The state of the review by this user. |
| `reviewed` | [`Boolean!`](#boolean) | Whether this user has provided a review for this merge request. |
### `UserPermissions` ### `UserPermissions`
| Field | Type | Description | | Field | Type | Description |
...@@ -7320,6 +7396,17 @@ All possible ways to specify the API surface for an API fuzzing scan. ...@@ -7320,6 +7396,17 @@ All possible ways to specify the API surface for an API fuzzing scan.
| `OPENAPI` | The API surface is specified by a OPENAPI file. | | `OPENAPI` | The API surface is specified by a OPENAPI file. |
| `POSTMAN` | The API surface is specified by a POSTMAN file. | | `POSTMAN` | The API surface is specified by a POSTMAN file. |
### `ApprovalRuleType`
The kind of an approval rule.
| Value | Description |
| ----- | ----------- |
| `ANY_APPROVER` | A `any_approver` approval rule. |
| `CODE_OWNER` | A `code_owner` approval rule. |
| `REGULAR` | A `regular` approval rule. |
| `REPORT_APPROVER` | A `report_approver` approval rule. |
### `AvailabilityEnum` ### `AvailabilityEnum`
User availability status. User availability status.
...@@ -7802,6 +7889,15 @@ New state to apply to a merge request. ...@@ -7802,6 +7889,15 @@ New state to apply to a merge request.
| `CLOSED` | Close the merge request if it is open. | | `CLOSED` | Close the merge request if it is open. |
| `OPEN` | Open the merge request if it is closed. | | `OPEN` | Open the merge request if it is closed. |
### `MergeRequestReviewState`
State of a review of a GitLab merge request.
| Value | Description |
| ----- | ----------- |
| `REVIEWED` | The merge request is reviewed. |
| `UNREVIEWED` | The merge request is unreviewed. |
### `MergeRequestSort` ### `MergeRequestSort`
Values for sorting merge requests. Values for sorting merge requests.
...@@ -8513,6 +8609,15 @@ A `GitlabErrorTrackingDetailedErrorID` is a global ID. It is encoded as a string ...@@ -8513,6 +8609,15 @@ A `GitlabErrorTrackingDetailedErrorID` is a global ID. It is encoded as a string
An example `GitlabErrorTrackingDetailedErrorID` is: `"gid://gitlab/Gitlab::ErrorTracking::DetailedError/1"`. An example `GitlabErrorTrackingDetailedErrorID` is: `"gid://gitlab/Gitlab::ErrorTracking::DetailedError/1"`.
### `GlobalID`
A global identifier.
A global identifier represents an object uniquely across the application.
An example of such an identifier is `"gid://gitlab/User/1"`.
Global identifiers are encoded as strings.
### `GroupID` ### `GroupID`
A `GroupID` is a global ID. It is encoded as a string. A `GroupID` is a global ID. It is encoded as a string.
......
# frozen_string_literal: true
module EE
module Types
module UserMergeRequestInteractionType
extend ActiveSupport::Concern
prepended do
field :applicable_approval_rules,
[::Types::ApprovalRuleType],
null: true,
description: 'Approval rules that apply to this user for this merge request.'
end
end
end
end
# frozen_string_literal: true
module Types
class ApprovalRuleType < BaseObject
graphql_name 'ApprovalRule'
description 'Describes a rule for who can approve merge requests.'
authorize :read_approval_rule
field :id,
type: ::Types::GlobalIDType,
null: false,
description: 'ID of the rule.'
field :name,
type: GraphQL::STRING_TYPE,
null: true,
description: 'Name of the rule.'
field :type,
type: ::Types::ApprovalRuleTypeEnum,
null: true,
method: :rule_type,
description: 'Type of the rule.'
end
end
# frozen_string_literal: true
module Types
class ApprovalRuleTypeEnum < BaseEnum
# See: ApprovalMergeRequestRule, and ApprovalProjectRule
graphql_name 'ApprovalRuleType'
description 'The kind of an approval rule.'
from_rails_enum(
ApprovalProjectRule.rule_types.merge(ApprovalMergeRequestRule.rule_types),
description: 'A `%{name}` approval rule.'
)
end
end
...@@ -24,7 +24,7 @@ class ApprovalWrappedRule ...@@ -24,7 +24,7 @@ class ApprovalWrappedRule
def_delegators(:@approval_rule, def_delegators(:@approval_rule,
:regular?, :any_approver?, :code_owner?, :report_approver?, :regular?, :any_approver?, :code_owner?, :report_approver?,
:overridden?, :id, :name, :users, :groups, :code_owner, :overridden?, :id, :name, :users, :groups, :code_owner,
:source_rule, :rule_type, :approvals_required, :section) :source_rule, :rule_type, :approvals_required, :section, :to_global_id)
def self.wrap(merge_request, rule) def self.wrap(merge_request, rule)
if rule.any_approver? if rule.any_approver?
...@@ -51,6 +51,10 @@ class ApprovalWrappedRule ...@@ -51,6 +51,10 @@ class ApprovalWrappedRule
end end
end end
def declarative_policy_delegate
@approval_rule
end
# @return [Array<User>] of users who have approved the merge request # @return [Array<User>] of users who have approved the merge request
# #
# This is dynamically calculated unless it is persisted as `approved_approvers`. # This is dynamically calculated unless it is persisted as `approved_approvers`.
......
# frozen_string_literal: true
module EE
module Users
module MergeRequestInteraction
def applicable_approval_rules
return [] unless merge_request.project.licensed_feature_available?(:merge_request_approvers)
merge_request.applicable_approval_rules_for_user(user.id)
end
end
end
end
...@@ -8,4 +8,6 @@ class ApprovalMergeRequestRulePolicy < BasePolicy ...@@ -8,4 +8,6 @@ class ApprovalMergeRequestRulePolicy < BasePolicy
end end
rule { editable }.enable :edit_approval_rule rule { editable }.enable :edit_approval_rule
rule { can?(:read_merge_request) }.enable :read_approval_rule
end end
...@@ -8,4 +8,6 @@ class ApprovalProjectRulePolicy < BasePolicy ...@@ -8,4 +8,6 @@ class ApprovalProjectRulePolicy < BasePolicy
end end
rule { editable }.enable :edit_approval_rule rule { editable }.enable :edit_approval_rule
rule { can?(:read_project) }.enable :read_approval_rule
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:interaction) { ::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset) }
it 'has the expected fields' do
expected_fields = %w[
applicable_approval_rules
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
def resolve(field_name)
resolve_field(field_name, interaction, current_user: current_user)
end
describe '#applicable_approval_rules' do
subject { resolve(:applicable_approval_rules) }
before do
merge_request.clear_memoization(:approval_state)
end
context 'when there are no approval rules' do
it { is_expected.to be_empty }
end
context 'when there are approval rules' do
before do
create(:approval_merge_request_rule, merge_request: merge_request)
create(:code_owner_rule, merge_request: merge_request)
create(:any_approver_rule, merge_request: merge_request)
end
context 'when the feature is not available' do
it { is_expected.to be_empty }
end
context 'when the feature is available' do
before do
stub_licensed_features(merge_request_approvers: true)
end
it { is_expected.to be_empty }
context 'when the user is associated with a rule' do
let(:rule) { create(:code_owner_rule, merge_request: merge_request) }
before do
rule.users << user
end
it { is_expected.to contain_exactly(have_attributes(approval_rule: rule)) }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ApprovalRuleType'] do
it 'has the correct members' do
expect(described_class.values).to match(
'REGULAR' => have_attributes(
description: 'A `regular` approval rule.',
value: 'regular'
),
'CODE_OWNER' => have_attributes(
description: 'A `code_owner` approval rule.',
value: 'code_owner'
),
'REPORT_APPROVER' => have_attributes(
description: 'A `report_approver` approval rule.',
value: 'report_approver'
),
'ANY_APPROVER' => have_attributes(
description: 'A `any_approver` approval rule.',
value: 'any_approver'
)
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ApprovalRule'] do
let(:fields) { %i[id name type] }
it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_approval_rule) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Users::MergeRequestInteraction do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
subject { described_class.new(user: user, merge_request: merge_request) }
describe '#applicable_approval_rules' do
before do
merge_request.reset
merge_request.clear_memoization(:approval_state)
end
context 'when there are no approval rules' do
it { is_expected.to have_attributes(applicable_approval_rules: be_empty) }
end
context 'when there are approval rules' do
before do
create(:approval_merge_request_rule, merge_request: merge_request)
create(:code_owner_rule, merge_request: merge_request)
create(:any_approver_rule, merge_request: merge_request)
end
context 'when the feature is not available' do
it { is_expected.to have_attributes(applicable_approval_rules: be_empty) }
end
context 'when the feature is available' do
before do
stub_licensed_features(merge_request_approvers: true)
end
it { is_expected.to have_attributes(applicable_approval_rules: be_empty) }
context 'when the user is associated with a rule' do
let(:rule) { create(:code_owner_rule, merge_request: merge_request) }
before do
create(:code_owner_rule, merge_request: merge_request) # irrelevant rule
rule.users << user
end
specify do
is_expected.to have_attributes(
applicable_approval_rules: contain_exactly(
have_attributes(approval_rule: rule)
)
)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'MergeRequestReviewer' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
context 'when requesting information about MR interactions' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:query) do
graphql_query_for(
:project,
{ full_path: project.full_path },
query_graphql_field(
:merge_request,
{ iid: merge_request.iid.to_s },
query_nodes(
:reviewers,
"mergeRequestInteraction { #{all_graphql_fields_for('UserMergeRequestInteraction')} }"
)
)
)
end
let(:interaction) do
graphql_data_at(:project,
:merge_request,
:reviewers,
:nodes, 0,
:merge_request_interaction)
end
before do
merge_request.reviewers << user
end
context 'when the user does not have any applicable rules' do
it 'returns null data' do
post_graphql(query)
expect(interaction).to include(
'applicableApprovalRules' => []
)
end
end
context 'when the user has interacted' do
let(:rule) { create(:code_owner_rule, merge_request: merge_request) }
before do
stub_licensed_features(merge_request_approvers: true)
rule.users << user
end
it 'returns appropriate data' do
the_rule = eq(
'id' => global_id_of(rule),
'name' => rule.name,
'type' => 'CODE_OWNER'
)
post_graphql(query)
expect(interaction['applicableApprovalRules']).to contain_exactly(the_rule)
end
end
end
end
...@@ -21,8 +21,9 @@ module Gitlab ...@@ -21,8 +21,9 @@ module Gitlab
def ok?(object, current_user) def ok?(object, current_user)
return true if none? return true if none?
subject = object.try(:declarative_policy_subject) || object
abilities.all? do |ability| abilities.all? do |ability|
Ability.allowed?(current_user, ability, object) Ability.allowed?(current_user, ability, subject)
end end
end end
end end
......
...@@ -3,6 +3,38 @@ ...@@ -3,6 +3,38 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Types::BaseEnum do RSpec.describe Types::BaseEnum do
describe '.from_rails_enum' do
let(:enum_type) { Class.new(described_class) }
let(:template) { "The name is '%{name}', James %{name}." }
let(:enum) do
{
'foo' => 1,
'bar' => 2,
'baz' => 100
}
end
it 'contructs the correct values' do
enum_type.from_rails_enum(enum, description: template)
expect(enum_type.values).to match(
'FOO' => have_attributes(
description: "The name is 'foo', James foo.",
value: 'foo'
),
'BAR' => have_attributes(
description: "The name is 'bar', James bar.",
value: 'bar'
),
'BAZ' => have_attributes(
description: "The name is 'baz', James baz.",
value: 'baz'
)
)
end
end
describe '.declarative_enum' do describe '.declarative_enum' do
let(:use_name) { true } let(:use_name) { true }
let(:use_description) { true } let(:use_description) { true }
...@@ -26,12 +58,15 @@ RSpec.describe Types::BaseEnum do ...@@ -26,12 +58,15 @@ RSpec.describe Types::BaseEnum do
end end
end end
subject(:set_declarative_enum) { enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description) } subject(:set_declarative_enum) do
enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description)
end
describe '#graphql_name' do describe '#graphql_name' do
context 'when the use_name is `true`' do context 'when the use_name is `true`' do
it 'changes the graphql_name' do it 'changes the graphql_name' do
expect { set_declarative_enum }.to change { enum_type.graphql_name }.from('OriginalName').to('Name') expect { set_declarative_enum }
.to change(enum_type, :graphql_name).from('OriginalName').to('Name')
end end
end end
...@@ -39,7 +74,8 @@ RSpec.describe Types::BaseEnum do ...@@ -39,7 +74,8 @@ RSpec.describe Types::BaseEnum do
let(:use_name) { false } let(:use_name) { false }
it 'does not change the graphql_name' do it 'does not change the graphql_name' do
expect { set_declarative_enum }.not_to change { enum_type.graphql_name }.from('OriginalName') expect { set_declarative_enum }
.not_to change(enum_type, :graphql_name).from('OriginalName')
end end
end end
end end
...@@ -47,7 +83,8 @@ RSpec.describe Types::BaseEnum do ...@@ -47,7 +83,8 @@ RSpec.describe Types::BaseEnum do
describe '#description' do describe '#description' do
context 'when the use_description is `true`' do context 'when the use_description is `true`' do
it 'changes the description' do it 'changes the description' do
expect { set_declarative_enum }.to change { enum_type.description }.from('Original description').to('Description') expect { set_declarative_enum }
.to change(enum_type, :description).from('Original description').to('Description')
end end
end end
...@@ -55,7 +92,8 @@ RSpec.describe Types::BaseEnum do ...@@ -55,7 +92,8 @@ RSpec.describe Types::BaseEnum do
let(:use_description) { false } let(:use_description) { false }
it 'does not change the description' do it 'does not change the description' do
expect { set_declarative_enum }.not_to change { enum_type.description }.from('Original description') expect { set_declarative_enum }
.not_to change(enum_type, :description).from('Original description')
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
it 'the correct enum members' do
expect(described_class.values).to match(
'REVIEWED' => have_attributes(
description: 'The merge request is reviewed.',
value: 'reviewed'
),
'UNREVIEWED' => have_attributes(
description: 'The merge request is unreviewed.',
value: 'unreviewed'
)
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['MergeRequestReviewer'] do
specify { expect(described_class).to require_graphql_authorizations(:read_user) }
it 'has the expected fields' do
expected_fields = %w[
id
bot
user_permissions
snippets
name
username
email
publicEmail
avatarUrl
webUrl
webPath
todos
state
status
location
authoredMergeRequests
assignedMergeRequests
reviewRequestedMergeRequests
groupMemberships
groupCount
projectMemberships
starredProjects
callouts
merge_request_interaction
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe '#merge_request_interaction' do
subject { described_class.fields['mergeRequestInteraction'] }
it 'returns the correct type' do
is_expected.to have_graphql_type(Types::UserMergeRequestInteractionType)
end
it 'has the correct arguments' do
is_expected.to have_attributes(arguments: be_empty)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:interaction) { ::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset) }
specify { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
it 'has the expected fields' do
expected_fields = %w[
can_merge
can_update
review_state
reviewed
approved
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
def resolve(field_name)
resolve_field(field_name, interaction, current_user: current_user)
end
describe '#can_merge' do
subject { resolve(:can_merge) }
context 'when the user cannot merge' do
it { is_expected.to be false }
end
context 'when the user can merge' do
before do
project.add_maintainer(user)
end
it { is_expected.to be true }
end
end
describe '#can_update' do
subject { resolve(:can_update) }
context 'when the user cannot update the MR' do
it { is_expected.to be false }
end
context 'when the user can update the MR' do
before do
project.add_developer(user)
end
it { is_expected.to be true }
end
end
describe '#review_state' do
subject { resolve(:review_state) }
context 'when the user has not been asked to review the MR' do
it { is_expected.to be_nil }
it 'implies not reviewed' do
expect(resolve(:reviewed)).to be false
end
end
context 'when the user has been asked to review the MR' do
before do
merge_request.reviewers << user
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
it 'implies not reviewed' do
expect(resolve(:reviewed)).to be false
end
end
context 'when the user has provided a review' do
before do
merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
it 'implies reviewed' do
expect(resolve(:reviewed)).to be true
end
end
end
describe '#approved' do
subject { resolve(:approved) }
context 'when the user has not approved the MR' do
it { is_expected.to be false }
end
context 'when the user has approved the MR' do
before do
merge_request.approved_by_users << user
end
it { is_expected.to be true }
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe ::Gitlab::Graphql::Authorize::ObjectAuthorization do
describe '#ok?' do
subject { described_class.new(%i[go_fast go_slow]) }
let(:user) { double(:User, id: 10001) }
let(:policy) do
Class.new(::DeclarativePolicy::Base) do
condition(:fast, scope: :subject) { @subject.x >= 10 }
condition(:slow, scope: :subject) { @subject.y >= 10 }
rule { fast }.policy do
enable :go_fast
end
rule { slow }.policy do
enable :go_slow
end
end
end
before do
stub_const('Foo', Struct.new(:x, :y))
stub_const('FooPolicy', policy)
end
context 'when there are no abilities' do
subject { described_class.new([]) }
it { is_expected.to be_ok(double, double) }
end
context 'when no ability should be allowed' do
let(:object) { Foo.new(0, 0) }
it { is_expected.not_to be_ok(object, user) }
end
context 'when go_fast should be allowed' do
let(:object) { Foo.new(100, 0) }
it { is_expected.not_to be_ok(object, user) }
end
context 'when go_fast and go_slow should be allowed' do
let(:object) { Foo.new(100, 100) }
it { is_expected.to be_ok(object, user) }
end
context 'when the object delegates to another subject' do
def proxy(foo)
double(:Proxy, declarative_policy_subject: foo)
end
it { is_expected.to be_ok(proxy(Foo.new(100, 100)), user) }
it { is_expected.not_to be_ok(proxy(Foo.new(0, 100)), user) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Users::MergeRequestInteraction do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
subject(:interaction) do
::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset)
end
describe 'declarative policy delegation' do
it 'delegates to the merge request' do
expect(subject.declarative_policy_subject).to eq(merge_request)
end
end
describe '#can_merge?' do
context 'when the user cannot merge' do
it { is_expected.not_to be_can_merge }
end
context 'when the user can merge' do
before do
project.add_maintainer(user)
end
it { is_expected.to be_can_merge }
end
end
describe '#can_update?' do
context 'when the user cannot update the MR' do
it { is_expected.not_to be_can_update }
end
context 'when the user can update the MR' do
before do
project.add_developer(user)
end
it { is_expected.to be_can_update }
end
end
describe '#review_state' do
subject { interaction.review_state }
context 'when the user has not been asked to review the MR' do
it { is_expected.to be_nil }
it 'implies not reviewed' do
expect(interaction).not_to be_reviewed
end
end
context 'when the user has been asked to review the MR' do
before do
merge_request.reviewers << user
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
it 'implies not reviewed' do
expect(interaction).not_to be_reviewed
end
end
context 'when the user has provided a review' do
before do
merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
it 'implies reviewed' do
expect(interaction).to be_reviewed
end
end
end
describe '#approved?' do
context 'when the user has not approved the MR' do
it { is_expected.not_to be_approved }
end
context 'when the user has approved the MR' do
before do
merge_request.approved_by_users << user
end
it { is_expected.to be_approved }
end
end
end
...@@ -42,7 +42,13 @@ RSpec.describe 'User' do ...@@ -42,7 +42,13 @@ RSpec.describe 'User' do
end end
context 'when username and id parameter are used' do context 'when username and id parameter are used' do
let_it_be(:query) { graphql_query_for(:user, { id: current_user.to_global_id.to_s, username: current_user.username }, 'id') } let_it_be(:query) do
graphql_query_for(
:user,
{ id: current_user.to_global_id.to_s, username: current_user.username },
'id'
)
end
it 'displays an error' do it 'displays an error' do
post_graphql(query) post_graphql(query)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment