Commit e64f2227 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'req-graphql-update' into 'master'

GraphQL: Allow update of Requirement

See merge request gitlab-org/gitlab!27417
parents 179e4512 9b71d27b
......@@ -5046,6 +5046,7 @@ type Mutation {
will be destroyed during the update, and no Note will be returned
"""
updateNote(input: UpdateNoteInput!): UpdateNotePayload
updateRequirement(input: UpdateRequirementInput!): UpdateRequirementPayload
updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload
}
......@@ -8498,6 +8499,56 @@ type UpdateNotePayload {
note: Note
}
"""
Autogenerated input type of UpdateRequirement
"""
input UpdateRequirementInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the requirement to update
"""
iid: String!
"""
The project full path the requirement is associated with
"""
projectPath: ID!
"""
State of the requirement
"""
state: RequirementState
"""
Title of the requirement
"""
title: String
}
"""
Autogenerated return type of UpdateRequirement
"""
type UpdateRequirementPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The requirement after mutation
"""
requirement: Requirement
}
"""
Autogenerated input type of UpdateSnippet
"""
......
......@@ -15331,6 +15331,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateRequirement",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateRequirementInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateRequirementPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateSnippet",
"description": null,
......@@ -25662,6 +25689,142 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateRequirementInput",
"description": "Autogenerated input type of UpdateRequirement",
"fields": null,
"inputFields": [
{
"name": "title",
"description": "Title of the requirement",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "state",
"description": "State of the requirement",
"type": {
"kind": "ENUM",
"name": "RequirementState",
"ofType": null
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the requirement to update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "projectPath",
"description": "The project full path the requirement is associated with",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateRequirementPayload",
"description": "Autogenerated return type of UpdateRequirement",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "requirement",
"description": "The requirement after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Requirement",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSnippetInput",
......
......@@ -1372,6 +1372,16 @@ Autogenerated return type of UpdateNote
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
## UpdateRequirementPayload
Autogenerated return type of UpdateRequirement
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `requirement` | Requirement | The requirement after mutation |
## UpdateSnippetPayload
Autogenerated return type of UpdateSnippet
......
......@@ -15,6 +15,7 @@ module EE
mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::Requirements::Create
mount_mutation ::Mutations::Requirements::Update
end
end
end
......
......@@ -9,8 +9,7 @@ module Mutations
authorize :create_requirement
field :requirement,
Types::RequirementType,
field :requirement, Types::RequirementType,
null: true,
description: 'The requirement after mutation'
......
# frozen_string_literal: true
module Mutations
module Requirements
class Update < BaseMutation
include Mutations::ResolvesProject
graphql_name 'UpdateRequirement'
authorize :update_requirement
field :requirement, Types::RequirementType,
null: true,
description: 'The requirement after mutation'
argument :title, GraphQL::STRING_TYPE,
required: false,
description: 'Title of the requirement'
argument :state, Types::RequirementStateEnum,
required: false,
description: 'State of the requirement'
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: 'The iid of the requirement to update'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project full path the requirement is associated with'
def ready?(**args)
if args.values_at(:title, :state).compact.blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'title or state argument is required'
end
super
end
def resolve(args)
project_path = args.delete(:project_path)
requirement_iid = args.delete(:iid)
requirement = authorized_find!(project_path: project_path, iid: requirement_iid)
requirement = ::Requirements::UpdateService.new(
requirement.project,
context[:current_user],
args
).execute(requirement)
{
requirement: requirement.reset,
errors: errors_on_object(requirement)
}
end
private
def find_object(project_path:, iid:)
project = resolve_project(full_path: project_path)
resolver = Resolvers::RequirementsResolver
.single.new(object: project, context: context, field: nil)
resolver.resolve(iid: iid)
end
end
end
end
# frozen_string_literal: true
module Requirements
class UpdateService < BaseService
def execute(requirement)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_requirement, project)
attrs = whitelisted_requirement_params
requirement.update(attrs)
requirement
end
private
def whitelisted_requirement_params
params.slice(:title, :state)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Requirements::Update do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:requirement) { create(:requirement, project: project) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
shared_examples 'requirements not available' do
it 'raises a not accessible error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
subject do
mutation.resolve(
project_path: project.full_path,
iid: requirement.iid.to_s,
title: 'foo',
state: 'archived'
)
end
it_behaves_like 'requirements not available'
context 'when the user can update the epic' do
before do
project.add_developer(user)
end
context 'when requirements feature is available' do
before do
stub_licensed_features(requirements: true)
end
it 'updates new requirement', :aggregate_failures do
expect(subject[:requirement]).to have_attributes(
title: 'foo',
state: 'archived'
)
expect(subject[:errors]).to be_empty
end
context 'when requirements_management flag is disabled' do
before do
stub_feature_flags(requirements_management: false)
end
it_behaves_like 'requirements not available'
end
end
context 'when requirements feature is disabled' do
before do
stub_licensed_features(requirements: false)
end
it_behaves_like 'requirements not available'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Updating a Requirement' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:requirement) { create(:requirement, project: project) }
let(:attributes) { { title: 'title', state: 'ARCHIVED' } }
let(:mutation) do
params = { project_path: project.full_path, iid: requirement.iid.to_s }.merge(attributes)
graphql_mutation(:update_requirement, params)
end
shared_examples 'requirement update fails' do
it_behaves_like 'a mutation that returns top-level errors',
errors: ['The resource that you are attempting to access does not exist '\
'or you don\'t have permission to perform this action']
it 'does not update requirement' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { requirement.reload }
end
end
def mutation_response
graphql_mutation_response(:update_requirement)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(requirements: true)
end
it_behaves_like 'requirement update fails'
end
context 'when the user has permission' do
before do
project.add_reporter(current_user)
end
context 'when requirements are disabled' do
before do
stub_licensed_features(requirements: false)
end
it_behaves_like 'requirement update fails'
end
context 'when requirements are enabled' do
before do
stub_licensed_features(requirements: true)
end
it 'updates the requirement', :aggregate_failures do
post_graphql_mutation(mutation, current_user: current_user)
requirement_hash = mutation_response['requirement']
expect(requirement_hash['title']).to eq('title')
expect(requirement_hash['state']).to eq('ARCHIVED')
end
context 'when there are ActiveRecord validation errors' do
let(:attributes) { { title: '' } }
it_behaves_like 'a mutation that returns errors in the response',
errors: ['Title can\'t be blank']
it 'does not update the requirement' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { requirement.reload }
end
end
context 'when there are no update params' do
let(:attributes) { {} }
it_behaves_like 'a mutation that returns top-level errors',
errors: ['title or state argument is required']
end
context 'when requirements_management flag is disabled' do
before do
stub_feature_flags(requirements_management: false)
end
it_behaves_like 'requirement update fails'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Requirements::UpdateService do
let_it_be(:project) { create(:project)}
let_it_be(:user) { create(:user) }
let_it_be(:requirement) { create(:requirement, project: project) }
let(:params) do
{
title: 'foo',
state: 'archived',
created_at: 2.days.ago,
author_id: create(:user).id
}
end
subject { described_class.new(project, user, params).execute(requirement) }
describe '#execute' do
before do
stub_licensed_features(requirements: true)
end
context 'when user can update requirements' do
before do
project.add_reporter(user)
end
it 'updates the requirement with only permitted params', :aggregate_failures do
is_expected.to have_attributes(
errors: be_empty,
title: params[:title],
state: params[:state]
)
is_expected.not_to have_attributes(
created_at: params[:created_at],
author_id: params[:author_id]
)
end
end
context 'when user is not allowed to update requirements' do
it 'raises an exception' do
expect { subject }.to raise_exception(Gitlab::Access::AccessDeniedError)
end
end
end
end
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