Commit 8d2e1d05 authored by Jan Provaznik's avatar Jan Provaznik Committed by Stan Hu

Allow creating epic board lists

Adds mutation and service for creating epic board lists.  Because most
of the functionality is same as for issue boar lists, it also moves
shared logic into a parent class.
parent 41f7a751
# frozen_string_literal: true
module Mutations
module Boards
module Lists
class BaseCreate < BaseMutation
argument :backlog, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Create the backlog list.'
argument :label_id, ::Types::GlobalIDType[::Label],
required: false,
description: 'Global ID of an existing label.'
def ready?(**args)
if args.slice(*mutually_exclusive_args).size != 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
def resolve(**args)
board = authorized_find!(id: args[:board_id])
params = create_list_params(args)
response = create_list(board, params)
{
list: response.success? ? response.payload[:list] : nil,
errors: response.errors
}
end
private
def create_list(board, params)
raise NotImplementedError
end
def create_list_params(args)
params = args.slice(*mutually_exclusive_args).with_indifferent_access
params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
params
end
def mutually_exclusive_args
[:backlog, :label_id]
end
end
end
end
end
...@@ -3,59 +3,32 @@ ...@@ -3,59 +3,32 @@
module Mutations module Mutations
module Boards module Boards
module Lists module Lists
class Create < Base class Create < BaseCreate
graphql_name 'BoardListCreate' graphql_name 'BoardListCreate'
argument :backlog, GraphQL::BOOLEAN_TYPE, argument :board_id, ::Types::GlobalIDType[::Board],
required: false, required: true,
description: 'Create the backlog list.' description: 'Global ID of the issue board to mutate.'
argument :label_id, ::Types::GlobalIDType[::Label], field :list,
required: false, Types::BoardListType,
description: 'Global ID of an existing label.' null: true,
description: 'Issue list in the issue board.'
def ready?(**args) authorize :admin_list
if args.slice(*mutually_exclusive_args).size != 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
def resolve(**args) private
board = authorized_find!(id: args[:board_id])
params = create_list_params(args)
response = create_list(board, params)
{ def find_object(id:)
list: response.success? ? response.payload[:list] : nil, GitlabSchema.object_from_id(id, expected_type: ::Board)
errors: response.errors
}
end end
private
def create_list(board, params) def create_list(board, params)
create_list_service = create_list_service =
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params) ::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
create_list_service.execute(board) create_list_service.execute(board)
end end
# Overridden in EE
def create_list_params(args)
params = args.slice(*mutually_exclusive_args).with_indifferent_access
params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
params
end
# Overridden in EE
def mutually_exclusive_args
[:backlog, :label_id]
end
end end
end end
end end
......
# frozen_string_literal: true
module Boards
module Lists
# This class is used by issue and epic board lists
# for creating new list
class BaseCreateService < Boards::BaseService
include Gitlab::Utils::StrongMemoize
def execute(board)
list = case type
when :backlog
create_backlog(board)
else
target = target(board)
position = next_position(board)
return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
create_list(board, type, target, position)
end
return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
ServiceResponse.success(payload: { list: list })
end
private
def type
# We don't ever expect to have more than one list
# type param at once.
if params.key?('backlog')
:backlog
else
:label
end
end
def target(board)
strong_memoize(:target) do
available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
end
end
def available_labels
::Labels::AvailableLabelsService.new(current_user, parent, {})
.available_labels
end
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
end
def create_list(board, type, target, position)
board.lists.create(create_list_attributes(type, target, position))
end
def create_list_attributes(type, target, position)
{ type => target, list_type: type, position: position }
end
def create_backlog(board)
return board.lists.backlog.first if board.lists.backlog.exists?
board.lists.create(list_type: :backlog, position: nil)
end
end
end
end
...@@ -2,68 +2,7 @@ ...@@ -2,68 +2,7 @@
module Boards module Boards
module Lists module Lists
class CreateService < Boards::BaseService class CreateService < Boards::Lists::BaseCreateService
include Gitlab::Utils::StrongMemoize
def execute(board)
list = case type
when :backlog
create_backlog(board)
else
target = target(board)
position = next_position(board)
return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
create_list(board, type, target, position)
end
return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
ServiceResponse.success(payload: { list: list })
end
private
def type
# We don't ever expect to have more than one list
# type param at once.
if params.key?('backlog')
:backlog
else
:label
end
end
def target(board)
strong_memoize(:target) do
available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
end
end
def available_labels
::Labels::AvailableLabelsService.new(current_user, parent, {})
.available_labels
end
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
end
def create_list(board, type, target, position)
board.lists.create(create_list_attributes(type, target, position))
end
def create_list_attributes(type, target, position)
{ type => target, list_type: type, position: position }
end
def create_backlog(board)
return board.lists.backlog.first if board.lists.backlog.exists?
board.lists.create(list_type: :backlog, position: nil)
end
end end
end end
end end
......
...@@ -2361,7 +2361,7 @@ type BoardListCreatePayload { ...@@ -2361,7 +2361,7 @@ type BoardListCreatePayload {
errors: [String!]! errors: [String!]!
""" """
List of the issue board. Issue list in the issue board.
""" """
list: BoardList list: BoardList
} }
...@@ -9155,6 +9155,51 @@ type EpicBoardEdge { ...@@ -9155,6 +9155,51 @@ type EpicBoardEdge {
node: EpicBoard node: EpicBoard
} }
"""
Autogenerated input type of EpicBoardListCreate
"""
input EpicBoardListCreateInput {
"""
Create the backlog list.
"""
backlog: Boolean
"""
Global ID of the issue board to mutate.
"""
boardId: BoardsEpicBoardID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of an existing label.
"""
labelId: LabelID
}
"""
Autogenerated return type of EpicBoardListCreate
"""
type EpicBoardListCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
Epic list in the epic board.
"""
list: EpicList
}
""" """
The connection type for Epic. The connection type for Epic.
""" """
...@@ -16235,6 +16280,7 @@ type Mutation { ...@@ -16235,6 +16280,7 @@ type Mutation {
environmentsCanaryIngressUpdate(input: EnvironmentsCanaryIngressUpdateInput!): EnvironmentsCanaryIngressUpdatePayload environmentsCanaryIngressUpdate(input: EnvironmentsCanaryIngressUpdateInput!): EnvironmentsCanaryIngressUpdatePayload
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicBoardCreate(input: EpicBoardCreateInput!): EpicBoardCreatePayload epicBoardCreate(input: EpicBoardCreateInput!): EpicBoardCreatePayload
epicBoardListCreate(input: EpicBoardListCreateInput!): EpicBoardListCreatePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload
......
...@@ -6046,36 +6046,36 @@ ...@@ -6046,36 +6046,36 @@
"fields": null, "fields": null,
"inputFields": [ "inputFields": [
{ {
"name": "boardId", "name": "backlog",
"description": "Global ID of the issue board to mutate.", "description": "Create the backlog list.",
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "BoardID", "name": "Boolean",
"ofType": null "ofType": null
}
}, },
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "backlog", "name": "labelId",
"description": "Create the backlog list.", "description": "Global ID of an existing label.",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "LabelID",
"ofType": null "ofType": null
}, },
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "labelId", "name": "boardId",
"description": "Global ID of an existing label.", "description": "Global ID of the issue board to mutate.",
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "LabelID", "name": "BoardID",
"ofType": null "ofType": null
}
}, },
"defaultValue": null "defaultValue": null
}, },
...@@ -6171,7 +6171,7 @@ ...@@ -6171,7 +6171,7 @@
}, },
{ {
"name": "list", "name": "list",
"description": "List of the issue board.", "description": "Issue list in the issue board.",
"args": [ "args": [
], ],
...@@ -25291,6 +25291,128 @@ ...@@ -25291,6 +25291,128 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "EpicBoardListCreateInput",
"description": "Autogenerated input type of EpicBoardListCreate",
"fields": null,
"inputFields": [
{
"name": "backlog",
"description": "Create the backlog list.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "labelId",
"description": "Global ID of an existing label.",
"type": {
"kind": "SCALAR",
"name": "LabelID",
"ofType": null
},
"defaultValue": null
},
{
"name": "boardId",
"description": "Global ID of the issue board to mutate.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "BoardsEpicBoardID",
"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": "EpicBoardListCreatePayload",
"description": "Autogenerated return type of EpicBoardListCreate",
"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": "Errors encountered during execution of the mutation.",
"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": "list",
"description": "Epic list in the epic board.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "EpicList",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "EpicConnection", "name": "EpicConnection",
...@@ -46081,6 +46203,33 @@ ...@@ -46081,6 +46203,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "epicBoardListCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "EpicBoardListCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "EpicBoardListCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "epicSetSubscription", "name": "epicSetSubscription",
"description": null, "description": null,
...@@ -357,7 +357,7 @@ Autogenerated return type of BoardListCreate. ...@@ -357,7 +357,7 @@ Autogenerated return type of BoardListCreate.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `list` | BoardList | List of the issue board. | | `list` | BoardList | Issue list in the issue board. |
### BoardListUpdateLimitMetricsPayload ### BoardListUpdateLimitMetricsPayload
...@@ -1482,6 +1482,16 @@ Autogenerated return type of EpicBoardCreate. ...@@ -1482,6 +1482,16 @@ Autogenerated return type of EpicBoardCreate.
| `epicBoard` | EpicBoard | The created epic board. | | `epicBoard` | EpicBoard | The created epic board. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
### EpicBoardListCreatePayload
Autogenerated return type of EpicBoardListCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `list` | EpicList | Epic list in the epic board. |
### EpicDescendantCount ### EpicDescendantCount
Counts of descendent epics. Counts of descendent epics.
......
...@@ -37,6 +37,7 @@ module EE ...@@ -37,6 +37,7 @@ module EE
mount_mutation ::Mutations::Boards::Update mount_mutation ::Mutations::Boards::Update
mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences
mount_mutation ::Mutations::Boards::EpicBoards::Create mount_mutation ::Mutations::Boards::EpicBoards::Create
mount_mutation ::Mutations::Boards::EpicLists::Create
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject
mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject
......
...@@ -2,25 +2,32 @@ ...@@ -2,25 +2,32 @@
module Mutations module Mutations
module Boards module Boards
module Lists module EpicLists
class Base < BaseMutation class Create < ::Mutations::Boards::Lists::BaseCreate
include Mutations::ResolvesIssuable graphql_name 'EpicBoardListCreate'
argument :board_id, ::Types::GlobalIDType[::Board], argument :board_id, ::Types::GlobalIDType[::Boards::EpicBoard],
required: true, required: true,
description: 'Global ID of the issue board to mutate.' description: 'Global ID of the issue board to mutate.'
field :list, field :list,
Types::BoardListType, Types::Boards::EpicListType,
null: true, null: true,
description: 'List of the issue board.' description: 'Epic list in the epic board.'
authorize :admin_list authorize :admin_epic_list
private private
def find_object(id:) def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Board) GitlabSchema.object_from_id(id, expected_type: ::Boards::EpicBoard)
end
def create_list(board, params)
create_list_service =
::Boards::EpicLists::CreateService.new(board.group, current_user, params)
create_list_service.execute(board)
end end
end end
end end
......
...@@ -198,6 +198,7 @@ module EE ...@@ -198,6 +198,7 @@ module EE
enable :read_confidential_epic enable :read_confidential_epic
enable :destroy_epic_link enable :destroy_epic_link
enable :admin_epic_board enable :admin_epic_board
enable :admin_epic_list
end end
rule { reporter & subepics_available }.policy do rule { reporter & subepics_available }.policy do
...@@ -214,6 +215,7 @@ module EE ...@@ -214,6 +215,7 @@ module EE
prevent :admin_epic prevent :admin_epic
prevent :update_epic prevent :update_epic
prevent :destroy_epic prevent :destroy_epic
prevent :admin_epic_list
end end
rule { auditor }.policy do rule { auditor }.policy do
......
# frozen_string_literal: true
module Boards
module EpicLists
class CreateService < ::Boards::Lists::BaseCreateService
extend ::Gitlab::Utils::Override
override :execute
def execute(board)
unless Feature.enabled?(:epic_boards, board.group)
return ServiceResponse.error(message: 'Epic boards feature is not enabled.')
end
super
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Boards::EpicLists::Create do
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:epic_board, group: group) }
before do
stub_licensed_features(epics: true)
end
it_behaves_like 'board lists create mutation'
end
...@@ -7,7 +7,8 @@ RSpec.describe GroupPolicy do ...@@ -7,7 +7,8 @@ RSpec.describe GroupPolicy do
let(:epic_rules) do let(:epic_rules) do
%i(read_epic create_epic admin_epic destroy_epic read_confidential_epic %i(read_epic create_epic admin_epic destroy_epic read_confidential_epic
destroy_epic_link read_epic_board read_epic_list admin_epic_board) destroy_epic_link read_epic_board read_epic_list admin_epic_board
admin_epic_list)
end end
context 'when epics feature is disabled' do context 'when epics feature is disabled' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a label or backlog board list' do
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:epic_board, group: group) }
before do
stub_licensed_features(epics: true)
end
it_behaves_like 'board lists create request' do
let(:mutation_name) { :epic_board_list_create }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Boards::EpicLists::CreateService do
let_it_be(:parent) { create(:group) }
let_it_be(:board) { create(:epic_board, group: parent) }
let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
it_behaves_like 'board lists create service' do
def create_list(params)
create(:epic_list, params.merge(epic_board: board))
end
end
context 'when epic_boards feature flag is disabled' do
before do
stub_feature_flags(epic_boards: false)
end
it 'returns an error' do
response = described_class.new(parent, nil).execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include("Epic boards feature is not enabled.")
end
end
end
...@@ -3,84 +3,8 @@ ...@@ -3,84 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Mutations::Boards::Lists::Create do RSpec.describe Mutations::Boards::Lists::Create do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) } let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:current_user) { user }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
let(:list_create_params) { {} }
before_all do
group.add_reporter(user)
group.add_guest(guest)
end
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
describe '#ready?' do
it 'raises an error if required arguments are missing' do
expect { mutation.ready?(board_id: 'some id') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
it 'raises an error if too many required arguments are specified' do
expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
end
describe '#resolve' do
context 'with proper permissions' do
describe 'backlog list' do
let(:list_create_params) { { backlog: true } }
it 'creates one and only one backlog' do
expect { subject }.to change { board.lists.backlog.count }.from(0).to(1)
expect(board.lists.backlog.first.list_type).to eq 'backlog'
backlog_id = board.lists.backlog.first.id
expect { subject }.not_to change { board.lists.backlog.count }
expect(board.lists.backlog.last.id).to eq backlog_id
end
end
describe 'label list' do
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
end
let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
it 'creates a new issue board list for labels' do
expect { subject }.to change { board.lists.count }.from(1).to(2)
new_list = subject[:list]
expect(new_list.title).to eq dev_label.title
expect(new_list.position).to eq 0
end
context 'when label not found' do
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
it 'returns an error' do
expect(subject[:errors]).to include 'Label not found'
end
end
end
end
context 'without proper permissions' do
let(:current_user) { guest }
it 'raises an error' do it_behaves_like 'board lists create mutation'
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end end
...@@ -3,52 +3,10 @@ ...@@ -3,52 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Create a label or backlog board list' do RSpec.describe 'Create a label or backlog board list' do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) } let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
end
let(:current_user) { user }
let(:mutation) { graphql_mutation(:board_list_create, input) }
let(:mutation_response) { graphql_mutation_response(:board_list_create) }
context 'the user is not allowed to read board lists' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to admin board lists' do
before do
group.add_reporter(current_user)
end
describe 'backlog list' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it 'creates the list' do it_behaves_like 'board lists create request' do
post_graphql_mutation(mutation, current_user: current_user) let(:mutation_name) { :board_list_create }
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => nil, 'listType' => 'backlog')
end
end
describe 'label list' do
let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
end
end
end end
end end
...@@ -3,91 +3,12 @@ ...@@ -3,91 +3,12 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Boards::Lists::CreateService do RSpec.describe Boards::Lists::CreateService do
describe '#execute' do
shared_examples 'creating board lists' do
let_it_be(:user) { create(:user) }
before_all do
parent.add_developer(user)
end
subject(:service) { described_class.new(parent, user, label_id: label.id) }
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 0
end
end
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 0
end
end
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 2
end
end
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
list2 = service.execute(board).payload[:list]
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
end
end
context 'when provided label does not belong to the parent' do
it 'returns an error' do
label = create(:label, name: 'in-development')
service = described_class.new(parent, user, label_id: label.id)
response = service.execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include('Label not found')
end
end
context 'when backlog param is sent' do
it 'creates one and only one backlog list' do
service = described_class.new(parent, user, 'backlog' => true)
list = service.execute(board).payload[:list]
expect(list.list_type).to eq('backlog')
expect(list.position).to be_nil
expect(list).to be_valid
another_backlog = service.execute(board).payload[:list]
expect(another_backlog).to eq list
end
end
end
context 'when board parent is a project' do context 'when board parent is a project' do
let_it_be(:parent) { create(:project) } let_it_be(:parent) { create(:project) }
let_it_be(:board) { create(:board, project: parent) } let_it_be(:board) { create(:board, project: parent) }
let_it_be(:label) { create(:label, project: parent, name: 'in-progress') } let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
it_behaves_like 'creating board lists' it_behaves_like 'board lists create service'
end end
context 'when board parent is a group' do context 'when board parent is a group' do
...@@ -95,7 +16,10 @@ RSpec.describe Boards::Lists::CreateService do ...@@ -95,7 +16,10 @@ RSpec.describe Boards::Lists::CreateService do
let_it_be(:board) { create(:board, group: parent) } let_it_be(:board) { create(:board, group: parent) }
let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') } let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
it_behaves_like 'creating board lists' it_behaves_like 'board lists create service'
end end
def create_list(params)
create(:list, params.merge(board: board))
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'board lists create mutation' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:list_create_params) { {} }
subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
describe '#ready?' do
it 'raises an error if required arguments are missing' do
expect { mutation.ready?(board_id: 'some id') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
it 'raises an error if too many required arguments are specified' do
expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
end
describe '#resolve' do
context 'with proper permissions' do
before_all do
group.add_reporter(user)
end
describe 'backlog list' do
let(:list_create_params) { { backlog: true } }
it 'creates one and only one backlog' do
expect { subject }.to change { board.lists.backlog.count }.by(1)
expect(board.lists.backlog.first.list_type).to eq 'backlog'
backlog_id = board.lists.backlog.first.id
expect { subject }.not_to change { board.lists.backlog.count }
expect(board.lists.backlog.last.id).to eq backlog_id
end
end
describe 'label list' do
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
end
let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
it 'creates a new label board list' do
expect { subject }.to change { board.lists.count }.by(1)
new_list = subject[:list]
expect(new_list.title).to eq dev_label.title
expect(new_list.position).to eq 0
end
context 'when label not found' do
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
it 'returns an error' do
expect(subject[:errors]).to include 'Label not found'
end
end
end
end
context 'without proper permissions' do
before_all do
group.add_guest(user)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'board lists create request' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: group)
end
let(:mutation) { graphql_mutation(mutation_name, input) }
let(:mutation_response) { graphql_mutation_response(mutation_name) }
context 'the user is not allowed to read board lists' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to admin board lists' do
before do
group.add_reporter(current_user)
end
describe 'backlog list' do
let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => nil, 'listType' => 'backlog')
end
end
describe 'label list' do
let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
it 'creates the list' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['list'])
.to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'board lists create service' do
describe '#execute' do
let_it_be(:user) { create(:user) }
before_all do
parent.add_developer(user)
end
subject(:service) { described_class.new(parent, user, label_id: label.id) }
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 0
end
end
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 0
end
end
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create_list(position: 0)
create_list(position: 1)
response = service.execute(board)
expect(response.success?).to eq(true)
expect(response.payload[:list].position).to eq 2
end
end
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create_list(position: 0)
list2 = service.execute(board).payload[:list]
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
end
end
context 'when provided label does not belong to the parent' do
it 'returns an error' do
label = create(:label, name: 'in-development')
service = described_class.new(parent, user, label_id: label.id)
response = service.execute(board)
expect(response.success?).to eq(false)
expect(response.errors).to include('Label not found')
end
end
context 'when backlog param is sent' do
it 'creates one and only one backlog list' do
service = described_class.new(parent, user, 'backlog' => true)
list = service.execute(board).payload[:list]
expect(list.list_type).to eq('backlog')
expect(list.position).to be_nil
expect(list).to be_valid
another_backlog = service.execute(board).payload[:list]
expect(another_backlog).to eq list
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