2022-05-07 20:08:51 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
|
2022-05-07 20:08:51 +05:30
|
|
|
include GraphqlHelpers
|
|
|
|
|
|
|
|
let_it_be(:developer) { create(:user) }
|
2022-07-23 23:45:48 +05:30
|
|
|
let_it_be(:guest) { create(:user) }
|
|
|
|
let_it_be(:project) { create(:project, :private) }
|
2022-08-27 11:52:29 +05:30
|
|
|
let_it_be(:work_item) do
|
|
|
|
create(
|
|
|
|
:work_item,
|
|
|
|
project: project,
|
|
|
|
description: '- List item',
|
|
|
|
start_date: Date.today,
|
2022-10-11 01:57:18 +05:30
|
|
|
due_date: 1.week.from_now,
|
|
|
|
created_at: 1.week.ago,
|
|
|
|
last_edited_at: 1.day.ago,
|
|
|
|
last_edited_by: guest
|
2022-08-27 11:52:29 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
|
|
|
|
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
|
|
|
|
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
|
|
|
|
let_it_be(:child_link2) { create(:parent_link, work_item_parent: work_item, work_item: child_item2) }
|
2022-05-07 20:08:51 +05:30
|
|
|
|
|
|
|
let(:current_user) { developer }
|
|
|
|
let(:work_item_data) { graphql_data['workItem'] }
|
2022-08-27 11:52:29 +05:30
|
|
|
let(:work_item_fields) { all_graphql_fields_for('WorkItem', max_depth: 2) }
|
2022-05-07 20:08:51 +05:30
|
|
|
let(:global_id) { work_item.to_gid.to_s }
|
|
|
|
|
|
|
|
let(:query) do
|
|
|
|
graphql_query_for('workItem', { 'id' => global_id }, work_item_fields)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the user can read the work item' do
|
|
|
|
before do
|
2022-07-23 23:45:48 +05:30
|
|
|
project.add_developer(developer)
|
|
|
|
project.add_guest(guest)
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'a working graphql query'
|
|
|
|
|
|
|
|
it 'returns all fields' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'description' => work_item.description,
|
|
|
|
'id' => work_item.to_gid.to_s,
|
|
|
|
'iid' => work_item.iid.to_s,
|
|
|
|
'lockVersion' => work_item.lock_version,
|
|
|
|
'state' => "OPEN",
|
|
|
|
'title' => work_item.title,
|
2022-08-27 11:52:29 +05:30
|
|
|
'confidential' => work_item.confidential,
|
2022-07-16 23:28:13 +05:30
|
|
|
'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s),
|
2023-04-23 21:23:45 +05:30
|
|
|
'userPermissions' => {
|
|
|
|
'readWorkItem' => true,
|
|
|
|
'updateWorkItem' => true,
|
|
|
|
'deleteWorkItem' => false,
|
|
|
|
'adminWorkItem' => true
|
|
|
|
},
|
2022-08-27 11:52:29 +05:30
|
|
|
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
|
2022-05-07 20:08:51 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2022-07-23 23:45:48 +05:30
|
|
|
context 'when querying widgets' do
|
|
|
|
describe 'description widget' do
|
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
|
|
|
... on WorkItemWidgetDescription {
|
|
|
|
description
|
|
|
|
descriptionHtml
|
2022-10-11 01:57:18 +05:30
|
|
|
edited
|
|
|
|
lastEditedBy {
|
|
|
|
webPath
|
|
|
|
username
|
|
|
|
}
|
|
|
|
lastEditedAt
|
2022-07-23 23:45:48 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
2022-08-13 15:12:31 +05:30
|
|
|
'widgets' => include(
|
2022-07-23 23:45:48 +05:30
|
|
|
hash_including(
|
|
|
|
'type' => 'DESCRIPTION',
|
|
|
|
'description' => work_item.description,
|
2022-10-11 01:57:18 +05:30
|
|
|
'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}),
|
|
|
|
'edited' => true,
|
|
|
|
'lastEditedAt' => work_item.last_edited_at.iso8601,
|
|
|
|
'lastEditedBy' => {
|
|
|
|
'webPath' => "/#{guest.full_path}",
|
|
|
|
'username' => guest.username
|
|
|
|
}
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'hierarchy widget' do
|
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
|
|
|
... on WorkItemWidgetHierarchy {
|
|
|
|
parent {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
children {
|
|
|
|
nodes {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
2023-03-04 22:38:38 +05:30
|
|
|
hasChildren
|
2022-07-23 23:45:48 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
2022-08-13 15:12:31 +05:30
|
|
|
'widgets' => include(
|
2022-07-23 23:45:48 +05:30
|
|
|
hash_including(
|
|
|
|
'type' => 'HIERARCHY',
|
|
|
|
'parent' => nil,
|
2022-11-25 23:54:43 +05:30
|
|
|
'children' => { 'nodes' => match_array(
|
|
|
|
[
|
|
|
|
hash_including('id' => child_link1.work_item.to_gid.to_s),
|
|
|
|
hash_including('id' => child_link2.work_item.to_gid.to_s)
|
2023-03-04 22:38:38 +05:30
|
|
|
]) },
|
|
|
|
'hasChildren' => true
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'avoids N+1 queries' do
|
|
|
|
post_graphql(query, current_user: current_user) # warm up
|
|
|
|
|
|
|
|
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
end
|
|
|
|
|
|
|
|
create_list(:parent_link, 3, work_item_parent: work_item)
|
|
|
|
|
|
|
|
expect do
|
|
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
end.not_to exceed_all_query_limit(control_count)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when user is guest' do
|
|
|
|
let(:current_user) { guest }
|
|
|
|
|
|
|
|
it 'filters out not accessible children or parent' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
2022-08-13 15:12:31 +05:30
|
|
|
'widgets' => include(
|
2022-07-23 23:45:48 +05:30
|
|
|
hash_including(
|
|
|
|
'type' => 'HIERARCHY',
|
|
|
|
'parent' => nil,
|
2022-11-25 23:54:43 +05:30
|
|
|
'children' => { 'nodes' => match_array(
|
|
|
|
[
|
|
|
|
hash_including('id' => child_link1.work_item.to_gid.to_s)
|
2023-03-04 22:38:38 +05:30
|
|
|
]) },
|
|
|
|
'hasChildren' => true
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when requesting child item' do
|
|
|
|
let_it_be(:work_item) { create(:work_item, :task, project: project, description: '- List item') }
|
|
|
|
let_it_be(:parent_link) { create(:parent_link, work_item: work_item) }
|
|
|
|
|
|
|
|
it 'returns parent information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
2022-08-13 15:12:31 +05:30
|
|
|
'widgets' => include(
|
2022-07-23 23:45:48 +05:30
|
|
|
hash_including(
|
|
|
|
'type' => 'HIERARCHY',
|
|
|
|
'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s),
|
2023-03-04 22:38:38 +05:30
|
|
|
'children' => { 'nodes' => match_array([]) },
|
|
|
|
'hasChildren' => false
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
2022-07-23 23:45:48 +05:30
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2023-03-17 16:20:25 +05:30
|
|
|
|
|
|
|
context 'when ordered by default by created_at' do
|
|
|
|
let_it_be(:newest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
|
|
|
|
let_it_be(:oldest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) }
|
|
|
|
let_it_be(:newest_link) { create(:parent_link, work_item_parent: work_item, work_item: newest_child) }
|
|
|
|
let_it_be(:oldest_link) { create(:parent_link, work_item_parent: work_item, work_item: oldest_child) }
|
|
|
|
|
|
|
|
let(:hierarchy_widget) { work_item_data['widgets'].find { |widget| widget['type'] == 'HIERARCHY' } }
|
|
|
|
let(:hierarchy_children) { hierarchy_widget['children']['nodes'] }
|
|
|
|
|
|
|
|
it 'places the oldest child item to the beginning of the children list' do
|
|
|
|
expect(hierarchy_children.first['id']).to eq(oldest_child.to_gid.to_s)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'places the newest child item to the end of the children list' do
|
|
|
|
expect(hierarchy_children.last['id']).to eq(newest_child.to_gid.to_s)
|
|
|
|
end
|
2023-04-23 21:23:45 +05:30
|
|
|
|
|
|
|
context 'when relative position is set' do
|
|
|
|
let_it_be(:first_child) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
|
|
|
|
|
|
|
|
let_it_be(:first_link) do
|
|
|
|
create(:parent_link, work_item_parent: work_item, work_item: first_child, relative_position: 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'places children according to relative_position at the beginning of the children list' do
|
|
|
|
ordered_list = [first_child, oldest_child, child_item1, child_item2, newest_child]
|
|
|
|
|
|
|
|
expect(hierarchy_children.pluck('id')).to eq(ordered_list.map(&:to_gid).map(&:to_s))
|
|
|
|
end
|
|
|
|
end
|
2023-03-17 16:20:25 +05:30
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
end
|
2022-08-13 15:12:31 +05:30
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
describe 'assignees widget' do
|
|
|
|
let(:assignees) { create_list(:user, 2) }
|
|
|
|
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
|
|
|
|
|
2022-08-13 15:12:31 +05:30
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
2022-08-27 11:52:29 +05:30
|
|
|
... on WorkItemWidgetAssignees {
|
|
|
|
allowsMultipleAssignees
|
|
|
|
canInviteMembers
|
|
|
|
assignees {
|
|
|
|
nodes {
|
|
|
|
id
|
|
|
|
username
|
|
|
|
}
|
|
|
|
}
|
2022-08-13 15:12:31 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
|
|
|
'widgets' => include(
|
|
|
|
hash_including(
|
2022-08-27 11:52:29 +05:30
|
|
|
'type' => 'ASSIGNEES',
|
|
|
|
'allowsMultipleAssignees' => boolean,
|
|
|
|
'canInviteMembers' => boolean,
|
|
|
|
'assignees' => {
|
|
|
|
'nodes' => match_array(
|
|
|
|
assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } }
|
|
|
|
)
|
|
|
|
}
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-08-27 11:52:29 +05:30
|
|
|
describe 'labels widget' do
|
|
|
|
let(:labels) { create_list(:label, 2, project: project) }
|
|
|
|
let(:work_item) { create(:work_item, project: project, labels: labels) }
|
2022-08-13 15:12:31 +05:30
|
|
|
|
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
2022-08-27 11:52:29 +05:30
|
|
|
... on WorkItemWidgetLabels {
|
|
|
|
labels {
|
2022-08-13 15:12:31 +05:30
|
|
|
nodes {
|
|
|
|
id
|
2022-08-27 11:52:29 +05:30
|
|
|
title
|
2022-08-13 15:12:31 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
|
|
|
'widgets' => include(
|
|
|
|
hash_including(
|
2022-08-27 11:52:29 +05:30
|
|
|
'type' => 'LABELS',
|
|
|
|
'labels' => {
|
2022-08-13 15:12:31 +05:30
|
|
|
'nodes' => match_array(
|
2022-08-27 11:52:29 +05:30
|
|
|
labels.map { |a| { 'id' => a.to_gid.to_s, 'title' => a.title } }
|
2022-08-13 15:12:31 +05:30
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2022-08-27 11:52:29 +05:30
|
|
|
|
|
|
|
describe 'start and due date widget' do
|
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
|
|
|
... on WorkItemWidgetStartAndDueDate {
|
|
|
|
startDate
|
|
|
|
dueDate
|
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
|
|
|
'widgets' => include(
|
|
|
|
hash_including(
|
|
|
|
'type' => 'START_AND_DUE_DATE',
|
|
|
|
'startDate' => work_item.start_date.to_s,
|
|
|
|
'dueDate' => work_item.due_date.to_s
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2023-01-13 00:05:48 +05:30
|
|
|
|
|
|
|
describe 'milestone widget' do
|
|
|
|
let_it_be(:milestone) { create(:milestone, project: project) }
|
|
|
|
|
|
|
|
let(:work_item) { create(:work_item, project: project, milestone: milestone) }
|
|
|
|
|
|
|
|
let(:work_item_fields) do
|
|
|
|
<<~GRAPHQL
|
|
|
|
id
|
|
|
|
widgets {
|
|
|
|
type
|
|
|
|
... on WorkItemWidgetMilestone {
|
|
|
|
milestone {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
GRAPHQL
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns widget information' do
|
|
|
|
expect(work_item_data).to include(
|
|
|
|
'id' => work_item.to_gid.to_s,
|
|
|
|
'widgets' => include(
|
|
|
|
hash_including(
|
|
|
|
'type' => 'MILESTONE',
|
|
|
|
'milestone' => {
|
|
|
|
'id' => work_item.milestone.to_gid.to_s
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2022-07-23 23:45:48 +05:30
|
|
|
end
|
|
|
|
|
2022-05-07 20:08:51 +05:30
|
|
|
context 'when an Issue Global ID is provided' do
|
|
|
|
let(:global_id) { Issue.find(work_item.id).to_gid.to_s }
|
|
|
|
|
|
|
|
it 'allows an Issue GID as input' do
|
|
|
|
expect(work_item_data).to include('id' => work_item.to_gid.to_s)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the user can not read the work item' do
|
|
|
|
let(:current_user) { create(:user) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
post_graphql(query)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns an access error' do
|
|
|
|
expect(work_item_data).to be_nil
|
|
|
|
expect(graphql_errors).to contain_exactly(
|
|
|
|
hash_including('message' => ::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|