1751 lines
56 KiB
Ruby
1751 lines
56 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
|
|
include GraphqlHelpers
|
|
|
|
let_it_be(:group) { create(:group) }
|
|
let_it_be_with_reload(:project) { create(:project, :repository, :private, group: group) }
|
|
let_it_be(:developer) { create(:user, developer_of: group) }
|
|
let_it_be(:guest) { create(:user, guest_of: group) }
|
|
let_it_be(:work_item) do
|
|
create(
|
|
:work_item,
|
|
project: project,
|
|
description: '- [x] List item',
|
|
start_date: Date.today,
|
|
due_date: 1.week.from_now,
|
|
created_at: 1.week.ago,
|
|
last_edited_at: 1.day.ago,
|
|
last_edited_by: guest,
|
|
user_agent_detail: create(:user_agent_detail)
|
|
).tap do |work_item|
|
|
create_list(:discussion_note_on_issue, 3, noteable: work_item, project: project)
|
|
end
|
|
end
|
|
|
|
let_it_be(:child_item1) { create(:work_item, :task, project: project, id: 1200) }
|
|
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project, id: 1400) }
|
|
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) }
|
|
|
|
let(:current_user) { developer }
|
|
let(:work_item_data) { graphql_data['workItem'] }
|
|
let(:work_item_fields) { all_graphql_fields_for('WorkItem', max_depth: 2) }
|
|
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 project is archived' do
|
|
before do
|
|
project.update!(archived: true)
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it 'returns the correct value in the archived field' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'iid' => work_item.iid.to_s,
|
|
'archived' => true
|
|
)
|
|
end
|
|
end
|
|
|
|
context "for showPlanUpgradePromotion field" do
|
|
context "when the namespace is in a free plan" do
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it "returns true" do
|
|
# For FOSS/ce version the api will always return true
|
|
expect(work_item_data).to include('showPlanUpgradePromotion' => true)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the user can read the work item' do
|
|
let(:incoming_email_token) { current_user.incoming_email_token }
|
|
let(:work_item_email) do
|
|
"p+#{project.full_path_slug}-#{project.project_id}-#{incoming_email_token}-issue-#{work_item.iid}@gl.ab"
|
|
end
|
|
|
|
before do
|
|
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
|
|
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,
|
|
'confidential' => work_item.confidential,
|
|
'userDiscussionsCount' => 3,
|
|
'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s),
|
|
'reference' => work_item.to_reference,
|
|
'createNoteEmail' => work_item_email,
|
|
'archived' => false,
|
|
'hidden' => false,
|
|
'userPermissions' => hash_including(
|
|
'readWorkItem' => true,
|
|
'updateWorkItem' => true,
|
|
'deleteWorkItem' => false,
|
|
'adminWorkItem' => true,
|
|
'adminParentLink' => true,
|
|
'setWorkItemMetadata' => true,
|
|
'createNote' => true,
|
|
'adminWorkItemLink' => true,
|
|
'markNoteAsInternal' => true,
|
|
'moveWorkItem' => true,
|
|
'cloneWorkItem' => true,
|
|
'reportSpam' => false,
|
|
'summarizeComments' => false
|
|
),
|
|
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
|
|
)
|
|
end
|
|
|
|
context 'when querying work item type information' do
|
|
include_context 'with work item types request context'
|
|
|
|
let(:work_item_fields) { "workItemType { #{work_item_type_fields} }" }
|
|
|
|
it 'returns work item type information' do
|
|
expect(work_item_data['workItemType']).to match(
|
|
expected_work_item_type_response(work_item.resource_parent, current_user, work_item.work_item_type).first
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when querying widgets' do
|
|
describe 'description widget' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDescription {
|
|
description
|
|
descriptionHtml
|
|
edited
|
|
lastEditedBy {
|
|
webPath
|
|
username
|
|
}
|
|
lastEditedAt
|
|
taskCompletionStatus {
|
|
completedCount
|
|
count
|
|
}
|
|
}
|
|
}
|
|
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' => 'DESCRIPTION',
|
|
'description' => work_item.description,
|
|
'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}),
|
|
'edited' => true,
|
|
'lastEditedAt' => work_item.last_edited_at.iso8601,
|
|
'lastEditedBy' => {
|
|
'webPath' => "/#{guest.full_path}",
|
|
'username' => guest.username
|
|
},
|
|
'taskCompletionStatus' => {
|
|
'completedCount' => 1,
|
|
'count' => 1
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe 'hierarchy widget' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetHierarchy {
|
|
parent {
|
|
id
|
|
}
|
|
children {
|
|
nodes {
|
|
id
|
|
}
|
|
}
|
|
hasChildren
|
|
hasParent
|
|
rolledUpCountsByType {
|
|
workItemType {
|
|
name
|
|
}
|
|
countsByState {
|
|
all
|
|
opened
|
|
closed
|
|
}
|
|
}
|
|
depthLimitReachedByType {
|
|
workItemType {
|
|
name
|
|
}
|
|
depthLimitReached
|
|
}
|
|
}
|
|
}
|
|
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' => 'HIERARCHY',
|
|
'parent' => nil,
|
|
'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)
|
|
]) },
|
|
'hasChildren' => true,
|
|
'hasParent' => false,
|
|
'rolledUpCountsByType' => match_array([
|
|
hash_including(
|
|
'workItemType' => hash_including('name' => 'Task'),
|
|
'countsByState' => {
|
|
'all' => 2,
|
|
'opened' => 2,
|
|
'closed' => 0
|
|
}
|
|
)
|
|
]),
|
|
'depthLimitReachedByType' => match_array([
|
|
hash_including(
|
|
'workItemType' => hash_including('name' => 'Task'),
|
|
'depthLimitReached' => false
|
|
)
|
|
])
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
it 'avoids N+1 queries' do
|
|
post_graphql(query, current_user: current_user) # warm up
|
|
|
|
control = 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)
|
|
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,
|
|
'widgets' => include(
|
|
hash_including(
|
|
'type' => 'HIERARCHY',
|
|
'parent' => nil,
|
|
'children' => { 'nodes' => match_array(
|
|
[
|
|
hash_including('id' => child_link1.work_item.to_gid.to_s)
|
|
]) },
|
|
'hasChildren' => true,
|
|
'hasParent' => false
|
|
)
|
|
)
|
|
)
|
|
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,
|
|
'widgets' => include(
|
|
hash_including(
|
|
'type' => 'HIERARCHY',
|
|
'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s),
|
|
'children' => { 'nodes' => match_array([]) },
|
|
'hasChildren' => false,
|
|
'hasParent' => true
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when ordered by default by work_item_id' do
|
|
let_it_be(:newest_child) { create(:work_item, :task, project: project, id: 2000) }
|
|
let_it_be(:oldest_child) { create(:work_item, :task, project: project, id: 1000) }
|
|
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
|
|
|
|
context 'when relative position is set' do
|
|
let_it_be(:first_child) { create(:work_item, :task, project: project, id: 3000) }
|
|
|
|
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
|
|
end
|
|
end
|
|
|
|
describe 'assignees widget' do
|
|
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
|
|
let(:assignees) do
|
|
[
|
|
create(:user, name: 'BBB'),
|
|
create(:user, name: 'AAA'),
|
|
create(:user, name: 'BBB')
|
|
]
|
|
end
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetAssignees {
|
|
allowsMultipleAssignees
|
|
canInviteMembers
|
|
assignees {
|
|
nodes {
|
|
id
|
|
username
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'returns widget information, assignees are ordered by name ASC id DESC' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'widgets' => include(
|
|
hash_including(
|
|
'type' => 'ASSIGNEES',
|
|
'allowsMultipleAssignees' => boolean,
|
|
'canInviteMembers' => boolean,
|
|
'assignees' => {
|
|
'nodes' => [
|
|
{ 'id' => assignees[1].to_gid.to_s, 'username' => assignees[1].username },
|
|
{ 'id' => assignees[2].to_gid.to_s, 'username' => assignees[2].username },
|
|
{ 'id' => assignees[0].to_gid.to_s, 'username' => assignees[0].username }
|
|
]
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe 'labels widget' do
|
|
let(:labels) { create_list(:label, 2, project: project) }
|
|
let(:work_item) { create(:work_item, project: project, labels: labels) }
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetLabels {
|
|
labels {
|
|
nodes {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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' => 'LABELS',
|
|
'labels' => {
|
|
'nodes' => match_array(
|
|
labels.map { |a| { 'id' => a.to_gid.to_s, 'title' => a.title } }
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
describe 'notifications widget' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetNotifications {
|
|
subscribed
|
|
}
|
|
}
|
|
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' => 'NOTIFICATIONS',
|
|
'subscribed' => work_item.subscribed?(current_user, project)
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe 'currentUserTodos widget' do
|
|
let_it_be(:current_user) { developer }
|
|
let_it_be(:other_todo) { create(:todo, state: :pending, user: current_user) }
|
|
|
|
let_it_be(:done_todo) do
|
|
create(:todo, state: :done, target: work_item, target_type: work_item.class.name, user: current_user)
|
|
end
|
|
|
|
let_it_be(:pending_todo) do
|
|
create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: current_user)
|
|
end
|
|
|
|
let_it_be(:other_user_todo) do
|
|
create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: create(:user))
|
|
end
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetCurrentUserTodos {
|
|
currentUserTodos {
|
|
nodes {
|
|
id
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
context 'with access' do
|
|
it 'returns widget information' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'widgets' => include(
|
|
hash_including(
|
|
'type' => 'CURRENT_USER_TODOS',
|
|
'currentUserTodos' => {
|
|
'nodes' => match_array(
|
|
[done_todo, pending_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'with filter' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetCurrentUserTodos {
|
|
currentUserTodos(state: done) {
|
|
nodes {
|
|
id
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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' => 'CURRENT_USER_TODOS',
|
|
'currentUserTodos' => {
|
|
'nodes' => match_array(
|
|
[done_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'award emoji widget' do
|
|
let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) }
|
|
let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) }
|
|
let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) }
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetAwardEmoji {
|
|
upvotes
|
|
downvotes
|
|
newCustomEmojiPath
|
|
awardEmoji {
|
|
nodes {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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' => 'AWARD_EMOJI',
|
|
'upvotes' => work_item.upvotes,
|
|
'downvotes' => work_item.downvotes,
|
|
'newCustomEmojiPath' => Gitlab::Routing.url_helpers.new_group_custom_emoji_path(group),
|
|
'awardEmoji' => {
|
|
'nodes' => match_array(
|
|
[emoji, upvote, downvote].map { |e| { 'name' => e.name } }
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe 'linked items widget' do
|
|
let_it_be(:related_item) { create(:work_item, project: project) }
|
|
let_it_be(:blocked_item) { create(:work_item, project: project) }
|
|
let_it_be(:link1) do
|
|
create(:work_item_link, source: work_item, target: related_item, link_type: 'relates_to',
|
|
created_at: Time.current + 1.day)
|
|
end
|
|
|
|
let_it_be(:link2) do
|
|
create(:work_item_link, source: work_item, target: blocked_item, link_type: 'blocks',
|
|
created_at: Time.current + 2.days)
|
|
end
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetLinkedItems {
|
|
linkedItems {
|
|
nodes {
|
|
linkId
|
|
linkType
|
|
linkCreatedAt
|
|
linkUpdatedAt
|
|
workItem {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'returns widget information' do
|
|
expect(work_item_data).to include(
|
|
'widgets' => include(
|
|
hash_including(
|
|
'type' => 'LINKED_ITEMS',
|
|
'linkedItems' => { 'nodes' => match_array(
|
|
[
|
|
hash_including(
|
|
'linkId' => link1.to_gid.to_s, 'linkType' => 'relates_to',
|
|
'linkCreatedAt' => link1.created_at.iso8601, 'linkUpdatedAt' => link1.updated_at.iso8601,
|
|
'workItem' => { 'id' => related_item.to_gid.to_s }
|
|
),
|
|
hash_including(
|
|
'linkId' => link2.to_gid.to_s, 'linkType' => 'blocks',
|
|
'linkCreatedAt' => link2.created_at.iso8601, 'linkUpdatedAt' => link2.updated_at.iso8601,
|
|
'workItem' => { 'id' => blocked_item.to_gid.to_s }
|
|
)
|
|
]
|
|
) }
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
context 'when inaccessible links are present' do
|
|
let_it_be(:no_access_item) { create(:work_item, title: "PRIVATE", project: create(:project, :private)) }
|
|
|
|
before do
|
|
create(:work_item_link, source: work_item, target: no_access_item, link_type: 'relates_to')
|
|
end
|
|
|
|
it 'returns only items that the user has access to' do
|
|
expect(graphql_dig_at(work_item_data, :widgets, "linkedItems", "nodes", "linkId"))
|
|
.to match_array([link1.to_gid.to_s, link2.to_gid.to_s])
|
|
end
|
|
|
|
context 'with anonymous user on public project' do
|
|
let(:current_user) { nil }
|
|
|
|
before do
|
|
project.update!(visibility: ::Gitlab::VisibilityLevel::PUBLIC)
|
|
end
|
|
|
|
it 'returns only items that the user has access to' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(graphql_dig_at(work_item_data, :widgets, "linkedItems", "nodes", "linkId"))
|
|
.to match_array([link1.to_gid.to_s, link2.to_gid.to_s])
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when limiting the number of results' do
|
|
it_behaves_like 'sorted paginated query' do
|
|
include_context 'no sort argument'
|
|
|
|
let(:first_param) { 1 }
|
|
let(:all_records) { [link2, link1] }
|
|
let(:data_path) { %w[workItem widgets linkedItems] }
|
|
|
|
def widget_fields(args)
|
|
query_graphql_field(
|
|
:widgets, {}, query_graphql_field(
|
|
'... on WorkItemWidgetLinkedItems', {}, query_graphql_field(
|
|
'linkedItems', args, "#{page_info} nodes { linkId }"
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
def pagination_query(params)
|
|
graphql_query_for('workItem', { 'id' => global_id }, widget_fields(params))
|
|
end
|
|
|
|
def pagination_results_data(nodes)
|
|
nodes.map { |item| GlobalID::Locator.locate(item['linkId']) }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by link type' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetLinkedItems {
|
|
linkedItems(filter: RELATED) {
|
|
nodes {
|
|
linkType
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'returns items with specified type' do
|
|
widget_data = work_item_data["widgets"].find { |widget| widget.key?("linkedItems") }["linkedItems"]
|
|
|
|
expect(widget_data["nodes"].size).to eq(1)
|
|
expect(widget_data.dig("nodes", 0, "linkType")).to eq('relates_to')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'linked resources widget' do
|
|
let_it_be(:linked_resources_type) { create(:work_item_type, :non_default, widgets: [:linked_resources]) }
|
|
let_it_be(:work_item) { create(:work_item, project: project, work_item_type: linked_resources_type) }
|
|
let_it_be(:resource1) do
|
|
create(:zoom_meeting, issue_id: work_item.id, project: project, url: 'https://zoom.us/j/123456789')
|
|
end
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetLinkedResources {
|
|
linkedResources {
|
|
nodes {
|
|
url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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' => 'LINKED_RESOURCES',
|
|
'linkedResources' => {
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'url' => resource1.url
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when filtering' do
|
|
context 'when selecting widgets' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets(onlyTypes: [DESCRIPTION]) {
|
|
type
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'only returns selected widgets' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'widgets' => [{
|
|
'type' => 'DESCRIPTION'
|
|
}]
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when excluding widgets' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets(exceptTypes: [DESCRIPTION]) {
|
|
type
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'does not return excluded widgets' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'widgets' => [
|
|
{ "type" => "ASSIGNEES" },
|
|
{ "type" => "AWARD_EMOJI" },
|
|
{ "type" => "CRM_CONTACTS" },
|
|
{ "type" => "CURRENT_USER_TODOS" },
|
|
{ "type" => "DESIGNS" },
|
|
{ "type" => "DEVELOPMENT" },
|
|
{ "type" => "EMAIL_PARTICIPANTS" },
|
|
{ "type" => "ERROR_TRACKING" },
|
|
{ "type" => "HIERARCHY" },
|
|
{ "type" => "LABELS" },
|
|
{ "type" => "LINKED_ITEMS" },
|
|
{ "type" => "MILESTONE" },
|
|
{ "type" => "NOTES" },
|
|
{ "type" => "NOTIFICATIONS" },
|
|
{ "type" => "PARTICIPANTS" },
|
|
{ "type" => "START_AND_DUE_DATE" },
|
|
{ "type" => "TIME_TRACKING" },
|
|
{ "type" => "LINKED_RESOURCES" }
|
|
]
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'notes widget' do
|
|
context 'when fetching award emoji from notes' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetNotes {
|
|
discussions(filter: ALL_NOTES, first: 10) {
|
|
nodes {
|
|
id
|
|
resolvable
|
|
resolvedBy {
|
|
username
|
|
}
|
|
userPermissions {
|
|
resolveNote
|
|
}
|
|
notes {
|
|
nodes {
|
|
id
|
|
system
|
|
body
|
|
maxAccessLevelOfAuthor
|
|
authorIsContributor
|
|
awardEmoji {
|
|
nodes {
|
|
name
|
|
user {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
discussion {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
notes(last: 1) {
|
|
nodes {
|
|
id
|
|
body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
let_it_be(:note) { create(:note, project: work_item.project, noteable: work_item, author: developer) }
|
|
|
|
before_all do
|
|
create(:award_emoji, awardable: note, name: 'rocket', user: developer)
|
|
end
|
|
|
|
context 'when fetching resolvable notes data' do
|
|
context 'with system notes' do
|
|
let_it_be(:comment1) { create(:discussion_note_on_issue, project: work_item.project, noteable: work_item) }
|
|
let_it_be(:comment2) { create(:note, discussion_id: comment1.discussion_id) }
|
|
let_it_be(:sys_note1) { create(:system_note, project: work_item.project, noteable: work_item) }
|
|
let_it_be(:sys_note2) { create(:resource_state_event, user: developer, issue: work_item, state: :closed) }
|
|
|
|
it 'returns resolve note permission' do
|
|
all_widgets = graphql_dig_at(work_item_data, :widgets)
|
|
notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
|
|
discussions = graphql_dig_at(notes_widget['discussions'], :nodes)
|
|
|
|
expect(discussions).to include(
|
|
hash_including(
|
|
'id' => note.discussion.to_global_id.to_s,
|
|
'resolvable' => false,
|
|
'userPermissions' => {
|
|
'resolveNote' => true
|
|
}
|
|
),
|
|
hash_including(
|
|
'id' => comment1.discussion.to_global_id.to_s,
|
|
'resolvable' => true,
|
|
'userPermissions' => {
|
|
'resolveNote' => true
|
|
}
|
|
),
|
|
hash_including(
|
|
'id' => sys_note1.discussion.to_global_id.to_s,
|
|
'resolvable' => false,
|
|
'userPermissions' => {
|
|
'resolveNote' => false
|
|
}
|
|
),
|
|
hash_including(
|
|
'id' => sys_note2.work_item_synthetic_system_note.discussion.to_global_id.to_s,
|
|
'resolvable' => false,
|
|
'userPermissions' => {
|
|
'resolveNote' => false
|
|
}
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'returns award emoji data' do
|
|
all_widgets = graphql_dig_at(work_item_data, :widgets)
|
|
notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
|
|
notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
|
|
|
|
note_with_emoji = notes.find { |n| n['id'] == note.to_gid.to_s }
|
|
|
|
expect(note_with_emoji).to include(
|
|
'awardEmoji' => {
|
|
'nodes' => include(
|
|
hash_including(
|
|
'name' => 'rocket',
|
|
'user' => {
|
|
'name' => developer.name
|
|
}
|
|
)
|
|
)
|
|
}
|
|
)
|
|
end
|
|
|
|
it 'returns author contributor status and max access level' do
|
|
all_widgets = graphql_dig_at(work_item_data, :widgets)
|
|
notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
|
|
notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
|
|
|
|
expect(notes).to include(
|
|
hash_including('maxAccessLevelOfAuthor' => 'Developer', 'authorIsContributor' => false)
|
|
)
|
|
end
|
|
|
|
it 'can return the latest note' do
|
|
latest_note = create(:note, project: work_item.project, noteable: work_item, note: 'Last note')
|
|
|
|
post_graphql(query, current_user: developer)
|
|
|
|
all_widgets = graphql_dig_at(work_item_data, :widgets)
|
|
notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
|
|
note = graphql_dig_at(notes_widget['notes'], :nodes).last
|
|
|
|
expect(note).to include(
|
|
'id' => latest_note.to_gid.to_s,
|
|
'body' => latest_note.note
|
|
)
|
|
end
|
|
|
|
it 'avoids N+1 queries' do
|
|
another_user = create(:user, developer_of: note.resource_parent)
|
|
create(:note, project: note.project, noteable: work_item, author: another_user)
|
|
|
|
post_graphql(query, current_user: developer)
|
|
|
|
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: developer) }
|
|
|
|
expect_graphql_errors_to_be_empty
|
|
|
|
another_note = create(:discussion_note, project: work_item.project, noteable: work_item)
|
|
create(:note, project: work_item.project, noteable: work_item, in_reply_to: another_note)
|
|
|
|
create(:award_emoji, awardable: another_note, name: 'star', user: guest)
|
|
another_user = create(:user, developer_of: note.resource_parent)
|
|
note_with_different_user = create(:note, project: note.project, noteable: work_item, author: another_user)
|
|
create(:award_emoji, awardable: note_with_different_user, name: 'star', user: developer)
|
|
|
|
expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control)
|
|
expect_graphql_errors_to_be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'designs widget' do
|
|
include DesignManagementTestHelpers
|
|
|
|
let(:work_item_fields) do
|
|
query_graphql_field(
|
|
:widgets, {}, query_graphql_field(
|
|
'type ... on WorkItemWidgetDesigns', {}, query_graphql_field(
|
|
:design_collection, nil, design_collection_fields
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
let(:design_collection_fields) { nil }
|
|
|
|
let(:post_query) { post_graphql(query, current_user: current_user) }
|
|
|
|
let(:design_collection_data) { work_item_data['widgets'].find { |w| w['type'] == 'DESIGNS' }['designCollection'] }
|
|
|
|
before do
|
|
project.add_developer(developer)
|
|
enable_design_management
|
|
end
|
|
|
|
def id_hash(object)
|
|
a_graphql_entity_for(object)
|
|
end
|
|
|
|
shared_examples 'fetch a design-like object by ID' do
|
|
let(:design) { design_a }
|
|
|
|
let(:design_fields) do
|
|
[
|
|
:filename,
|
|
query_graphql_field(:project, :id)
|
|
]
|
|
end
|
|
|
|
let(:design_collection_fields) do
|
|
query_graphql_field(object_field_name, object_params, object_fields)
|
|
end
|
|
|
|
let(:object_fields) { design_fields }
|
|
|
|
context 'when the ID is passed' do
|
|
let(:object_params) { { id: global_id_of(object) } }
|
|
let(:result_fields) { {} }
|
|
|
|
it 'retrieves the object' do
|
|
post_query
|
|
data = design_collection_data[GraphqlHelpers.fieldnamerize(object_field_name)]
|
|
|
|
expect(data).to match(
|
|
a_hash_including(
|
|
result_fields.merge({ 'filename' => design.filename, 'project' => id_hash(project) })
|
|
)
|
|
)
|
|
end
|
|
|
|
context 'when the user is unauthorized' do
|
|
let(:current_user) { create(:user) }
|
|
|
|
it_behaves_like 'a failure to find anything'
|
|
end
|
|
|
|
context 'without parameters' do
|
|
let(:object_params) { nil }
|
|
|
|
it 'raises an error' do
|
|
post_query
|
|
|
|
expect(graphql_errors).to include(no_argument_error)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when attempting to retrieve an object from a different issue' do
|
|
let(:object_params) { { id: global_id_of(object_on_other_issue) } }
|
|
|
|
it_behaves_like 'a failure to find anything'
|
|
end
|
|
end
|
|
|
|
context 'when work item is an issue' do
|
|
let_it_be(:issue_work_item) { create(:work_item, :issue, project: project) }
|
|
let_it_be(:issue_work_item1) { create(:work_item, :issue, project: project) }
|
|
let_it_be(:design_a) { create(:design, issue: issue_work_item) }
|
|
let_it_be(:version_a) { create(:design_version, issue: issue_work_item, created_designs: [design_a]) }
|
|
let_it_be(:global_id) { issue_work_item.to_gid.to_s }
|
|
|
|
describe '.designs' do
|
|
let(:design_collection_fields) do
|
|
query_graphql_field('designs', {}, "nodes { id event filename }")
|
|
end
|
|
|
|
it 'returns design data' do
|
|
post_query
|
|
|
|
expect(design_collection_data).to include(
|
|
'designs' => include(
|
|
'nodes' => include(
|
|
hash_including(
|
|
'id' => design_a.to_gid.to_s,
|
|
'event' => 'CREATION',
|
|
'filename' => design_a.filename
|
|
)
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe 'copy_state' do
|
|
let(:design_collection_fields) do
|
|
'copyState'
|
|
end
|
|
|
|
it 'returns copyState of designCollection' do
|
|
post_query
|
|
|
|
expect(design_collection_data).to include(
|
|
'copyState' => 'READY'
|
|
)
|
|
end
|
|
end
|
|
|
|
describe '.versions' do
|
|
let(:design_collection_fields) do
|
|
query_graphql_field('versions', {}, "nodes { id sha createdAt }")
|
|
end
|
|
|
|
it 'returns versions data' do
|
|
post_query
|
|
|
|
expect(design_collection_data).to include(
|
|
'versions' => include(
|
|
'nodes' => include(
|
|
hash_including(
|
|
'id' => version_a.to_gid.to_s,
|
|
'sha' => version_a.sha,
|
|
'createdAt' => version_a.created_at.iso8601
|
|
)
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
describe '.version' do
|
|
let(:version) { version_a }
|
|
|
|
let(:design_collection_fields) do
|
|
query_graphql_field(:version, version_params, 'id sha')
|
|
end
|
|
|
|
context 'with no parameters' do
|
|
let(:version_params) { nil }
|
|
|
|
it 'raises an error' do
|
|
post_query
|
|
|
|
expect(graphql_errors).to include(a_hash_including("message" => "one of id or sha is required"))
|
|
end
|
|
end
|
|
|
|
shared_examples 'a successful query for a version' do
|
|
it 'finds the version' do
|
|
post_query
|
|
|
|
data = design_collection_data['version']
|
|
|
|
expect(data).to match a_graphql_entity_for(version, :sha)
|
|
end
|
|
end
|
|
|
|
context 'with (sha: STRING_TYPE)' do
|
|
let(:version_params) { { sha: version.sha } }
|
|
|
|
it_behaves_like 'a successful query for a version'
|
|
end
|
|
|
|
context 'with (id: ID_TYPE)' do
|
|
let(:version_params) { { id: global_id_of(version) } }
|
|
|
|
it_behaves_like 'a successful query for a version'
|
|
end
|
|
end
|
|
|
|
describe '.design' do
|
|
it_behaves_like 'fetch a design-like object by ID' do
|
|
let(:object) { design }
|
|
let(:object_field_name) { :design }
|
|
|
|
let(:no_argument_error) do
|
|
a_hash_including("message" => "one of id or filename must be passed")
|
|
end
|
|
|
|
let_it_be(:object_on_other_issue) { create(:design, issue: issue_work_item1) }
|
|
end
|
|
end
|
|
|
|
describe '.designAtVersion' do
|
|
it_behaves_like 'fetch a design-like object by ID' do
|
|
let(:object) { build(:design_at_version, design: design, version: version) }
|
|
let(:object_field_name) { :design_at_version }
|
|
|
|
let(:version) { version_a }
|
|
|
|
let(:result_fields) { { 'version' => id_hash(version) } }
|
|
let(:object_fields) do
|
|
design_fields + [query_graphql_field(:version, :id)]
|
|
end
|
|
|
|
let(:no_argument_error) do
|
|
a_hash_including("message" => "Field 'designAtVersion' is missing required arguments: id")
|
|
end
|
|
|
|
let(:object_on_other_issue) { build(:design_at_version, issue: issue_work_item1) }
|
|
end
|
|
end
|
|
|
|
describe 'N+1 query check' do
|
|
let(:design_collection_fields) do
|
|
query_graphql_field('designs', {}, "nodes { id event filename}")
|
|
end
|
|
|
|
it 'avoids N+1 queries', :use_sql_query_cache do
|
|
post_query # warmup
|
|
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_query
|
|
end
|
|
|
|
create_list(:work_item, 3, namespace: group) do |item|
|
|
create(:design, :with_file, issue: item)
|
|
end
|
|
|
|
expect do
|
|
post_query
|
|
end.to issue_same_number_of_queries_as(control_count)
|
|
expect_graphql_errors_to_be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when work item base type is non issue' do
|
|
let_it_be(:epic) { create(:work_item, :task, project: project) }
|
|
let_it_be(:global_id) { epic.to_gid.to_s }
|
|
|
|
it 'returns without design' do
|
|
post_query
|
|
|
|
expect(epic&.work_item_type&.base_type).not_to match('issue')
|
|
expect(work_item_data['widgets']).not_to include(
|
|
hash_including(
|
|
'type' => 'DESIGNS'
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'development widget' do
|
|
let_it_be_with_reload(:merge_request1) { create(:merge_request, source_project: project) }
|
|
let_it_be_with_reload(:merge_request2) { create(:merge_request, source_project: project, target_branch: 'feat2') }
|
|
|
|
context 'when fetching related merge requests' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
relatedMergeRequests {
|
|
nodes {
|
|
id
|
|
iid
|
|
author { id username }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before_all do
|
|
update_params = { description: "References #{work_item.to_reference}" }
|
|
|
|
[merge_request1, merge_request2].each do |merge_request|
|
|
::MergeRequests::UpdateService
|
|
.new(project: merge_request.project, current_user: developer, params: update_params)
|
|
.execute(merge_request)
|
|
end
|
|
end
|
|
|
|
context 'when user is developer' do
|
|
let(:current_user) { developer }
|
|
|
|
it 'returns related merge requests in the response' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_global_id.to_s,
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'DEVELOPMENT',
|
|
'relatedMergeRequests' => {
|
|
'nodes' => [
|
|
hash_including('id' => merge_request2.to_gid.to_s, 'iid' => merge_request2.iid.to_s),
|
|
hash_including('id' => merge_request1.to_gid.to_s, 'iid' => merge_request1.iid.to_s)
|
|
]
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
it 'prevents N+1 queries' do
|
|
post_graphql(query, current_user: current_user) # warm up
|
|
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
merge_request3 = create(:merge_request, source_project: project, target_branch: 'feat3')
|
|
::MergeRequests::UpdateService.new(
|
|
project: merge_request3.project,
|
|
current_user: developer,
|
|
params: { description: "References #{work_item.to_reference}" }
|
|
).execute(merge_request3)
|
|
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.not_to exceed_all_query_limit(control)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when fetching closing merge requests' do
|
|
let_it_be(:private_project) { create(:project, :repository, :private) }
|
|
let_it_be(:private_merge_request) { create(:merge_request, source_project: private_project) }
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
willAutoCloseByMergeRequest
|
|
closingMergeRequests {
|
|
count
|
|
nodes {
|
|
id
|
|
fromMrDescription
|
|
mergeRequest { id }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
let_it_be(:mr_closing_issue1) do
|
|
create(
|
|
:merge_requests_closing_issues,
|
|
merge_request: merge_request1,
|
|
issue: work_item,
|
|
from_mr_description: false
|
|
)
|
|
end
|
|
|
|
let_it_be(:mr_closing_issue2) do
|
|
create(
|
|
:merge_requests_closing_issues,
|
|
merge_request: merge_request2,
|
|
issue: work_item,
|
|
from_mr_description: true
|
|
)
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
context 'when user is developer' do
|
|
let(:current_user) { developer }
|
|
|
|
it 'returns related merge requests in the response' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_global_id.to_s,
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'DEVELOPMENT',
|
|
'willAutoCloseByMergeRequest' => true,
|
|
'closingMergeRequests' => {
|
|
'count' => 2,
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'id' => mr_closing_issue1.to_gid.to_s,
|
|
'mergeRequest' => { 'id' => merge_request1.to_global_id.to_s },
|
|
'fromMrDescription' => false
|
|
),
|
|
hash_including(
|
|
'id' => mr_closing_issue2.to_gid.to_s,
|
|
'mergeRequest' => { 'id' => merge_request2.to_global_id.to_s },
|
|
'fromMrDescription' => true
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
it 'avoids N + 1 queries', :use_sql_query_cache do
|
|
# warm-up already done in the before block
|
|
control = ActiveRecord::QueryRecorder.new do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
expect(graphql_errors).to be_blank
|
|
|
|
create(
|
|
:merge_requests_closing_issues,
|
|
merge_request: create(:merge_request, source_project: project, target_branch: 'feature3'),
|
|
issue: work_item
|
|
)
|
|
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.to issue_same_number_of_queries_as(control)
|
|
expect(graphql_errors).to be_blank
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when fetching related branches' do
|
|
let_it_be(:branch_name) { "#{work_item.iid}-another-branch" }
|
|
let_it_be(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: work_item.to_branch_name) }
|
|
let_it_be(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch_name) }
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
relatedBranches {
|
|
nodes {
|
|
name
|
|
comparePath
|
|
pipelineStatus { name label favicon }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before_all do
|
|
project.repository.create_branch(work_item.to_branch_name, pipeline1.sha)
|
|
project.repository.create_branch(branch_name, pipeline2.sha)
|
|
project.repository.create_branch("#{work_item.iid}doesnt-match", project.repository.root_ref)
|
|
project.repository.create_branch("#{work_item.iid}-0-stable", project.repository.root_ref)
|
|
|
|
project.repository.add_tag(developer, work_item.to_branch_name, pipeline1.sha)
|
|
create(
|
|
:merge_request,
|
|
source_project: work_item.project,
|
|
source_branch: work_item.to_branch_name,
|
|
description: "Related to #{work_item.to_reference}"
|
|
).tap { |merge_request| merge_request.create_cross_references!(developer) }
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
context 'when user is developer' do
|
|
let(:current_user) { developer }
|
|
|
|
it 'returns related branches not referenced in merge requests' do
|
|
brach_compare_path = Gitlab::Routing.url_helpers.project_compare_path(
|
|
project,
|
|
from: project.default_branch,
|
|
to: branch_name
|
|
)
|
|
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_global_id.to_s,
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'DEVELOPMENT',
|
|
'relatedBranches' => {
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'name' => branch_name,
|
|
'comparePath' => brach_compare_path,
|
|
'pipelineStatus' => {
|
|
'name' => 'SUCCESS',
|
|
'label' => 'passed',
|
|
'favicon' => 'favicon_status_success'
|
|
}
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'email participants widget' do
|
|
let_it_be(:email) { 'user@example.com' }
|
|
let_it_be(:obfuscated_email) { 'us*****@e*****.c**' }
|
|
let_it_be(:issue_email_participant) { create(:issue_email_participant, issue_id: work_item.id, email: email) }
|
|
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetEmailParticipants {
|
|
emailParticipants {
|
|
nodes {
|
|
email
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'contains the email' do
|
|
expect(work_item_data).to include(
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'EMAIL_PARTICIPANTS',
|
|
'emailParticipants' => {
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'email' => email
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
context 'when user has the guest role' do
|
|
let(:current_user) { guest }
|
|
|
|
it 'contains the obfuscated email' do
|
|
expect(work_item_data).to include(
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'EMAIL_PARTICIPANTS',
|
|
'emailParticipants' => {
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'email' => obfuscated_email
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'contacts widget' do
|
|
let(:work_item_fields) do
|
|
<<~GRAPHQL
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetCrmContacts {
|
|
contactsAvailable
|
|
contacts {
|
|
nodes {
|
|
firstName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
context 'when no contacts are available' do
|
|
it 'returns expected data' do
|
|
expect(work_item_data).to include(
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'CRM_CONTACTS',
|
|
'contactsAvailable' => false,
|
|
'contacts' => {
|
|
'nodes' => be_empty
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when contacts are available' do
|
|
let_it_be(:contact) { create(:contact, group: work_item.project.group) }
|
|
let_it_be(:issue_contact) { create(:issue_customer_relations_contact, issue: work_item, contact: contact) }
|
|
|
|
it 'returns expected data' do
|
|
expect(work_item_data).to include(
|
|
'widgets' => array_including(
|
|
hash_including(
|
|
'type' => 'CRM_CONTACTS',
|
|
'contactsAvailable' => true,
|
|
'contacts' => {
|
|
'nodes' => containing_exactly(
|
|
hash_including(
|
|
'firstName' => contact.first_name
|
|
)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
context 'when the user cannot set work item metadata' do
|
|
let(:current_user) { guest }
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it 'returns correct user permission' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'userPermissions' =>
|
|
hash_including(
|
|
'setWorkItemMetadata' => false
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when the user can submit a work item as spam' do
|
|
let(:current_user) { create(:user, :admin) }
|
|
|
|
before do
|
|
stub_application_setting(akismet_enabled: true)
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it 'returns correct user permission' do
|
|
expect(work_item_data).to include(
|
|
'id' => work_item.to_gid.to_s,
|
|
'userPermissions' =>
|
|
hash_including(
|
|
'reportSpam' => true
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|