Commit 81d7073c authored by Jan Provaznik's avatar Jan Provaznik Committed by Sean McGivern

Add requirement types

These are present in another MR, but for now include them here
and then rebase.
parent dec11f02
...@@ -601,6 +601,46 @@ type CreateNotePayload { ...@@ -601,6 +601,46 @@ type CreateNotePayload {
note: Note note: Note
} }
"""
Autogenerated input type of CreateRequirement
"""
input CreateRequirementInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The project full path the requirement is associated with
"""
projectPath: ID!
"""
Title of the requirement
"""
title: String!
}
"""
Autogenerated return type of CreateRequirement
"""
type CreateRequirementPayload {
"""
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 CreateSnippet Autogenerated input type of CreateSnippet
""" """
...@@ -4952,6 +4992,7 @@ type Mutation { ...@@ -4952,6 +4992,7 @@ type Mutation {
createEpic(input: CreateEpicInput!): CreateEpicPayload createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
createNote(input: CreateNoteInput!): CreateNotePayload createNote(input: CreateNoteInput!): CreateNotePayload
createRequirement(input: CreateRequirementInput!): CreateRequirementPayload
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
...@@ -6574,6 +6615,94 @@ type Repository { ...@@ -6574,6 +6615,94 @@ type Repository {
): Tree ): Tree
} }
"""
Represents a requirement.
"""
type Requirement {
"""
Author of the requirement
"""
author: User!
"""
Timestamp of when the requirement was created
"""
createdAt: Time!
"""
ID of the requirement
"""
id: ID!
"""
Internal ID of the requirement
"""
iid: ID!
"""
Project to which the requirement belongs
"""
project: Project!
"""
State of the requirement
"""
state: RequirementState!
"""
Title of the requirement
"""
title: String
"""
Timestamp of when the requirement was last updated
"""
updatedAt: Time!
"""
Permissions for the current user on the resource
"""
userPermissions: RequirementPermissions!
}
"""
Check permissions for the current user on a requirement
"""
type RequirementPermissions {
"""
Indicates the user can perform `admin_requirement` on this resource
"""
adminRequirement: Boolean!
"""
Indicates the user can perform `create_requirement` on this resource
"""
createRequirement: Boolean!
"""
Indicates the user can perform `destroy_requirement` on this resource
"""
destroyRequirement: Boolean!
"""
Indicates the user can perform `read_requirement` on this resource
"""
readRequirement: Boolean!
"""
Indicates the user can perform `update_requirement` on this resource
"""
updateRequirement: Boolean!
}
"""
State of a requirement
"""
enum RequirementState {
ARCHIVED
OPENED
}
type RootStorageStatistics { type RootStorageStatistics {
""" """
The CI artifacts size in bytes The CI artifacts size in bytes
......
...@@ -1762,6 +1762,122 @@ ...@@ -1762,6 +1762,122 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "CreateRequirementInput",
"description": "Autogenerated input type of CreateRequirement",
"fields": null,
"inputFields": [
{
"name": "title",
"description": "Title of the requirement",
"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": "CreateRequirementPayload",
"description": "Autogenerated return type of CreateRequirement",
"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", "kind": "INPUT_OBJECT",
"name": "CreateSnippetInput", "name": "CreateSnippetInput",
...@@ -14390,6 +14506,33 @@ ...@@ -14390,6 +14506,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "createRequirement",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateRequirementInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "CreateRequirementPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "createSnippet", "name": "createSnippet",
"description": null, "description": null,
...@@ -19792,6 +19935,303 @@ ...@@ -19792,6 +19935,303 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "Requirement",
"description": "Represents a requirement.",
"fields": [
{
"name": "author",
"description": "Author of the requirement",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of when the requirement was created",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the requirement",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iid",
"description": "Internal ID of the requirement",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": "Project to which the requirement belongs",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the requirement",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "RequirementState",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "title",
"description": "Title of the requirement",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "Timestamp of when the requirement was last updated",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userPermissions",
"description": "Permissions for the current user on the resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RequirementPermissions",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RequirementPermissions",
"description": "Check permissions for the current user on a requirement",
"fields": [
{
"name": "adminRequirement",
"description": "Indicates the user can perform `admin_requirement` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createRequirement",
"description": "Indicates the user can perform `create_requirement` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyRequirement",
"description": "Indicates the user can perform `destroy_requirement` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "readRequirement",
"description": "Indicates the user can perform `read_requirement` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateRequirement",
"description": "Indicates the user can perform `update_requirement` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "RequirementState",
"description": "State of a requirement",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "OPENED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ARCHIVED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "RootStorageStatistics", "name": "RootStorageStatistics",
......
...@@ -129,6 +129,16 @@ Autogenerated return type of CreateNote ...@@ -129,6 +129,16 @@ Autogenerated return type of CreateNote
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation | | `note` | Note | The note after mutation |
## CreateRequirementPayload
Autogenerated return type of CreateRequirement
| 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 |
## CreateSnippetPayload ## CreateSnippetPayload
Autogenerated return type of CreateSnippet Autogenerated return type of CreateSnippet
...@@ -982,6 +992,34 @@ Autogenerated return type of RemoveAwardEmoji ...@@ -982,6 +992,34 @@ Autogenerated return type of RemoveAwardEmoji
| `rootRef` | String | Default branch of the repository | | `rootRef` | String | Default branch of the repository |
| `tree` | Tree | Tree of the repository | | `tree` | Tree | Tree of the repository |
## Requirement
Represents a requirement.
| Name | Type | Description |
| --- | ---- | ---------- |
| `author` | User! | Author of the requirement |
| `createdAt` | Time! | Timestamp of when the requirement was created |
| `id` | ID! | ID of the requirement |
| `iid` | ID! | Internal ID of the requirement |
| `project` | Project! | Project to which the requirement belongs |
| `state` | RequirementState! | State of the requirement |
| `title` | String | Title of the requirement |
| `updatedAt` | Time! | Timestamp of when the requirement was last updated |
| `userPermissions` | RequirementPermissions! | Permissions for the current user on the resource |
## RequirementPermissions
Check permissions for the current user on a requirement
| Name | Type | Description |
| --- | ---- | ---------- |
| `adminRequirement` | Boolean! | Indicates the user can perform `admin_requirement` on this resource |
| `createRequirement` | Boolean! | Indicates the user can perform `create_requirement` on this resource |
| `destroyRequirement` | Boolean! | Indicates the user can perform `destroy_requirement` on this resource |
| `readRequirement` | Boolean! | Indicates the user can perform `read_requirement` on this resource |
| `updateRequirement` | Boolean! | Indicates the user can perform `update_requirement` on this resource |
## RootStorageStatistics ## RootStorageStatistics
| Name | Type | Description | | Name | Type | Description |
......
...@@ -14,6 +14,7 @@ module EE ...@@ -14,6 +14,7 @@ module EE
mount_mutation ::Mutations::Epics::Create mount_mutation ::Mutations::Epics::Create
mount_mutation ::Mutations::Epics::SetSubscription mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::Requirements::Create
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module Requirements
class Create < BaseMutation
include Mutations::ResolvesProject
graphql_name 'CreateRequirement'
authorize :create_requirement
field :requirement,
Types::RequirementType,
null: true,
description: 'The requirement after mutation'
argument :title, GraphQL::STRING_TYPE,
required: true,
description: 'Title of the requirement'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project full path the requirement is associated with'
def resolve(args)
project_path = args.delete(:project_path)
project = authorized_find!(full_path: project_path)
validate_flag!(project)
requirement = ::Requirements::CreateService.new(
project,
context[:current_user],
args
).execute
{
requirement: requirement.valid? ? requirement : nil,
errors: errors_on_object(requirement)
}
end
private
def validate_flag!(project)
return if ::Feature.enabled?(:requirements_management, project)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'requirements_management flag is not enabled on this project'
end
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class Requirement < BasePermissionType
graphql_name 'RequirementPermissions'
description 'Check permissions for the current user on a requirement'
abilities :read_requirement, :update_requirement, :destroy_requirement,
:admin_requirement, :create_requirement
end
end
end
# frozen_string_literal: true
module Types
class RequirementStateEnum < BaseEnum
graphql_name 'RequirementState'
description 'State of a requirement'
value 'OPENED', value: 'opened'
value 'ARCHIVED', value: 'archived'
end
end
# frozen_string_literal: true
module Types
class RequirementType < BaseObject
graphql_name 'Requirement'
description 'Represents a requirement.'
authorize :read_requirement
expose_permissions Types::PermissionTypes::Requirement
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the requirement'
field :iid, GraphQL::ID_TYPE, null: false,
description: 'Internal ID of the requirement'
field :title, GraphQL::STRING_TYPE, null: true,
description: 'Title of the requirement'
field :state, RequirementStateEnum, null: false,
description: 'State of the requirement'
field :project, ProjectType, null: false,
description: 'Project to which the requirement belongs',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.project_id).find }
field :author, Types::UserType, null: false,
description: 'Author of the requirement',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of when the requirement was created'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of when the requirement was last updated'
end
end
# frozen_string_literal: true
module Requirements
class CreateService < BaseService
include Gitlab::Allowable
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :create_requirement, project)
attrs = whitelisted_requirement_params.merge(author: current_user)
project.requirements.create(attrs)
end
private
def whitelisted_requirement_params
params.slice(:title)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Requirements::Create do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
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,
title: 'foo'
)
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 'creates new requirement' do
expect(subject[:requirement][:title]).to eq('foo')
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 GitlabSchema.types['RequirementState'] do
it { expect(described_class.graphql_name).to eq('RequirementState') }
it 'exposes all the existing requirement states' do
expect(described_class.values.keys).to include(*%w[OPENED ARCHIVED])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Requirement'] do
fields = %i[id iid title state project author created_at updated_at user_permissions]
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Requirement) }
it { expect(described_class.graphql_name).to eq('Requirement') }
it { expect(described_class).to require_graphql_authorizations(:read_requirement) }
it { expect(described_class).to have_graphql_fields(fields) }
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Creating a Requirement' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:attributes) { { title: 'title' } }
let(:mutation) do
params = { project_path: project.full_path }.merge(attributes)
graphql_mutation(:create_requirement, params)
end
def mutation_response
graphql_mutation_response(:create_requirement)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(requirements: true)
end
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 create requirement' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Requirement, :count)
end
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 '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']
end
context 'when requirements are enabled' do
before do
stub_licensed_features(requirements: true)
end
it 'creates the requirement' 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('OPENED')
expect(requirement_hash['author']['username']).to eq(current_user.username)
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 create the requirement' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Requirement, :count)
end
end
context 'when requirements_management flag is dissabled' do
before do
stub_feature_flags(requirements_management: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ['requirements_management flag is not enabled on this project']
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Requirements::CreateService do
let_it_be(:project) { create(:project)}
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let(:params) { { title: 'foo', author_id: other_user.id, created_at: 2.days.ago } }
subject { described_class.new(project, user, params).execute }
describe '#execute' do
before do
stub_licensed_features(requirements: true)
end
context 'when user can create requirements' do
before do
project.add_reporter(user)
end
it 'creates new requirement' do
expect { subject }.to change { Requirement.count }.by(1)
end
it 'uses only permitted params' do
requirement = subject
expect(requirement).to be_persisted
expect(requirement.title).to eq(params[:title])
expect(requirement.state).to eq('opened')
expect(requirement.created_at).not_to eq(params[:created_at])
expect(requirement.author_id).not_to eq(params[:author_id])
end
end
context 'when user is not allowed to create 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