1130 lines
34 KiB
Ruby
1130 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe 'getting a work item list for a project', feature_category: :team_planning do
|
|
include_context 'with work items list request'
|
|
|
|
let_it_be(:label1) { create(:label, project: project) }
|
|
let_it_be(:label2) { create(:label, project: project) }
|
|
let_it_be(:milestone1) { create(:milestone, project: project, due_date: 5.days.ago) }
|
|
let_it_be(:milestone2) { create(:milestone, project: project, due_date: 3.days.from_now) }
|
|
|
|
let_it_be_with_reload(:item1) do
|
|
create(
|
|
:work_item, project: project, discussion_locked: true,
|
|
title: 'item1', milestone: milestone2, labels: [label1]
|
|
)
|
|
end
|
|
|
|
let_it_be_with_reload(:item2) do
|
|
create(
|
|
:work_item,
|
|
project: project,
|
|
title: 'item2',
|
|
last_edited_by: current_user,
|
|
last_edited_at: 1.day.ago,
|
|
labels: [label2],
|
|
milestone: milestone1
|
|
)
|
|
end
|
|
|
|
let_it_be_with_reload(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') }
|
|
let_it_be(:other_item) { create(:work_item) }
|
|
|
|
let(:items_data) { graphql_data['project']['workItems']['nodes'] }
|
|
let(:item_filter_params) { {} }
|
|
|
|
before_all do
|
|
# Ensure support bot user is created so creation doesn't count towards query limit
|
|
# and we don't try to obtain an exclusive lease within a transaction.
|
|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/509629
|
|
Users::Internal.support_bot_id
|
|
end
|
|
|
|
shared_examples 'work items resolver without N + 1 queries' do |threshold: 0|
|
|
it 'avoids N+1 queries', :use_sql_query_cache 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
|
|
|
|
expect_graphql_errors_to_be_empty
|
|
|
|
create_list(
|
|
:work_item, 3,
|
|
:task,
|
|
:last_edited_by_user,
|
|
last_edited_at: 1.week.ago,
|
|
project: project,
|
|
labels: [label1, label2],
|
|
milestone: milestone2,
|
|
author: reporter
|
|
)
|
|
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.not_to exceed_all_query_limit(control).with_threshold(threshold)
|
|
|
|
expect_graphql_errors_to_be_empty
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'graphql work item list request spec' do
|
|
let_it_be(:container_build_params) { { project: project } }
|
|
let(:work_item_node_path) { %w[project workItems nodes] }
|
|
|
|
def post_query(request_user = current_user)
|
|
post_graphql(query, current_user: request_user)
|
|
end
|
|
end
|
|
|
|
describe 'N + 1 queries' do
|
|
context 'when querying root fields' do
|
|
# Issue to fix N+1 - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
|
|
it_behaves_like 'work items resolver without N + 1 queries', threshold: 3
|
|
end
|
|
|
|
# We need a separate example since all_graphql_fields_for will not fetch fields from types
|
|
# that implement the widget interface. Only `type` for the widgets field.
|
|
context 'when querying the widget interface' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDescription {
|
|
edited
|
|
lastEditedAt
|
|
lastEditedBy {
|
|
webPath
|
|
username
|
|
}
|
|
}
|
|
... on WorkItemWidgetAssignees {
|
|
assignees { nodes { id } }
|
|
}
|
|
... on WorkItemWidgetHierarchy {
|
|
parent { id }
|
|
children {
|
|
nodes {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
... on WorkItemWidgetLabels {
|
|
labels { nodes { id } }
|
|
allowsScopedLabels
|
|
}
|
|
... on WorkItemWidgetMilestone {
|
|
milestone {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it_behaves_like 'work items resolver without N + 1 queries'
|
|
end
|
|
end
|
|
|
|
context 'when querying WorkItemWidgetAssignees' do
|
|
let(:work_items_data) { graphql_data['project']['workItems']['nodes'].pluck('widgets') }
|
|
let(:widget_data) { work_items_data.map { |data| data.find { |widget| widget['type'] == 'ASSIGNEES' } } }
|
|
let(:assignee_data) { widget_data.map { |data| data.dig('assignees', 'nodes') } }
|
|
let(:assignees) do
|
|
[
|
|
create(:user, name: 'BBB'),
|
|
create(:user, name: 'AAA'),
|
|
create(:user, name: 'BBB')
|
|
]
|
|
end
|
|
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetAssignees {
|
|
assignees { nodes { id } }
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before do
|
|
project.work_items.each { |work_item| work_item.update!(assignees: assignees) }
|
|
end
|
|
|
|
it 'returns assignees ordered by name ASC id DESC' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(assignee_data).to all(
|
|
eq(
|
|
[
|
|
{ 'id' => assignees[1].to_gid.to_s },
|
|
{ 'id' => assignees[2].to_gid.to_s },
|
|
{ 'id' => assignees[0].to_gid.to_s }
|
|
]
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when querying WorkItemWidgetHierarchy' do
|
|
let_it_be(:children) { create_list(:work_item, 4, :task, project: project) }
|
|
let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) }
|
|
let_it_be(:child_link2) { create(:parent_link, work_item_parent: item1, work_item: children[1]) }
|
|
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetHierarchy {
|
|
hasChildren
|
|
parent { id }
|
|
children { nodes { id } }
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
context 'with ordered children' do
|
|
let(:items_data) { graphql_data['project']['workItems']['nodes'] }
|
|
let(:work_item_data) { items_data.find { |item| item['id'] == item1.to_gid.to_s } }
|
|
let(:work_item_widget) { work_item_data["widgets"].find { |widget| widget.key?("children") } }
|
|
let(:children_ids) { work_item_widget.dig("children", "nodes").pluck("id") }
|
|
|
|
let(:first_child) { children[0].to_gid.to_s }
|
|
let(:second_child) { children[1].to_gid.to_s }
|
|
|
|
it 'returns children ordered by created_at by default' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(children_ids).to eq([first_child, second_child])
|
|
end
|
|
|
|
context 'when ordered by relative position' do
|
|
before do
|
|
child_link1.update!(relative_position: 20)
|
|
child_link2.update!(relative_position: 10)
|
|
end
|
|
|
|
it 'returns children in correct order' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(children_ids).to eq([second_child, first_child])
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'executes limited number of N+1 queries' do
|
|
post_graphql(query, current_user: current_user) # warm-up
|
|
|
|
control = ActiveRecord::QueryRecorder.new do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
parent_work_items = create_list(:work_item, 2, project: project)
|
|
create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[2])
|
|
create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[3])
|
|
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_query_limit(control)
|
|
end
|
|
|
|
it 'avoids N+1 queries when children are added to a work item' do
|
|
post_graphql(query, current_user: current_user) # warm-up
|
|
|
|
control = ActiveRecord::QueryRecorder.new do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create(:parent_link, work_item_parent: item1, work_item: children[2])
|
|
create(:parent_link, work_item_parent: item1, work_item: children[3])
|
|
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_query_limit(control)
|
|
end
|
|
end
|
|
|
|
context 'when querying WorkItemWidgetStartAndDueDate' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetStartAndDueDate {
|
|
dueDate
|
|
startDate
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'avoids N+1 queries when we create more work items' do
|
|
post_graphql(query, current_user: current_user) # warm-up
|
|
|
|
control = ActiveRecord::QueryRecorder.new do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create_list(:work_item, 3, project: project)
|
|
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_query_limit(control)
|
|
end
|
|
end
|
|
|
|
context 'when the user does not have access to the item' do
|
|
before do
|
|
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
|
|
end
|
|
|
|
it 'returns an empty list' do
|
|
post_graphql(query)
|
|
|
|
expect(items_data).to eq([])
|
|
end
|
|
end
|
|
|
|
it 'returns only items visible to user' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(item_ids).to eq([item2.to_global_id.to_s, item1.to_global_id.to_s])
|
|
end
|
|
|
|
context 'when the user can see confidential items' do
|
|
before do
|
|
project.add_developer(current_user)
|
|
end
|
|
|
|
it 'returns also confidential items' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(item_ids).to eq([confidential_item.to_global_id.to_s, item2.to_global_id.to_s, item1.to_global_id.to_s])
|
|
end
|
|
end
|
|
|
|
context 'when filtering by search' do
|
|
it_behaves_like 'query with a search term' do
|
|
let(:ids) { item_ids }
|
|
let(:user) { current_user }
|
|
let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') }
|
|
end
|
|
end
|
|
|
|
context 'when filtering by subscribed' do
|
|
let_it_be(:subscribed_item) { create(:work_item, project: project) }
|
|
let_it_be(:unsubscribed_item) { create(:work_item, project: project) }
|
|
let_it_be(:subscription) do
|
|
create(:subscription, subscribable: subscribed_item, user: current_user, subscribed: true)
|
|
end
|
|
|
|
let_it_be(:unsubscription) do
|
|
create(:subscription, subscribable: unsubscribed_item, user: current_user, subscribed: false)
|
|
end
|
|
|
|
it 'returns only subscribed items' do
|
|
post_graphql(query(subscribed: :EXPLICITLY_SUBSCRIBED), current_user: current_user)
|
|
|
|
expect(item_ids).to match_array([subscribed_item.to_global_id.to_s])
|
|
end
|
|
|
|
it 'returns only unsubscribed items' do
|
|
post_graphql(query(subscribed: :EXPLICITLY_UNSUBSCRIBED), current_user: current_user)
|
|
|
|
expect(item_ids).to match_array([unsubscribed_item.to_global_id.to_s])
|
|
end
|
|
|
|
it 'does not filter subscribed items' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(item_ids).to match_array([subscribed_item.to_global_id.to_s, unsubscribed_item.to_global_id.to_s,
|
|
item1.to_global_id.to_s, item2.to_global_id.to_s])
|
|
end
|
|
end
|
|
|
|
describe 'sorting and pagination' do
|
|
let(:data_path) { [:project, :work_items] }
|
|
|
|
def pagination_query(params)
|
|
graphql_query_for(
|
|
'project',
|
|
{ 'fullPath' => project.full_path },
|
|
query_graphql_field('workItems', params, "#{page_info} nodes { id }")
|
|
)
|
|
end
|
|
|
|
before do
|
|
project.add_developer(current_user)
|
|
end
|
|
|
|
context 'when sorting by title ascending' do
|
|
it_behaves_like 'sorted paginated query' do
|
|
let(:sort_param) { :TITLE_ASC }
|
|
let(:first_param) { 2 }
|
|
let(:all_records) { [item1, item2, confidential_item].map { |item| item.to_global_id.to_s } }
|
|
end
|
|
end
|
|
|
|
context 'when sorting by title descending' do
|
|
it_behaves_like 'sorted paginated query' do
|
|
let(:sort_param) { :TITLE_DESC }
|
|
let(:first_param) { 2 }
|
|
let(:all_records) { [confidential_item, item2, item1].map { |item| item.to_global_id.to_s } }
|
|
end
|
|
end
|
|
|
|
context 'when sorting by MILESTONE_DUE_ASC' do
|
|
it_behaves_like 'sorted paginated query' do
|
|
let(:sort_param) { :MILESTONE_DUE_ASC }
|
|
let(:first_param) { 2 }
|
|
let(:all_records) { [item2, item1, confidential_item].map { |item| item.to_global_id.to_s } }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when fetching work item notifications widget' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetNotifications {
|
|
subscribed
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'executes limited number of N+1 queries', :use_sql_query_cache do
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create_list(:work_item, 3, project: project)
|
|
|
|
# Performs 1 extra query per item to fetch subscriptions
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_all_query_limit(control).with_threshold(3)
|
|
expect_graphql_errors_to_be_empty
|
|
end
|
|
end
|
|
|
|
context 'when fetching work item award emoji widget' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetAwardEmoji {
|
|
awardEmoji {
|
|
nodes {
|
|
name
|
|
emoji
|
|
user { id }
|
|
}
|
|
}
|
|
upvotes
|
|
downvotes
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before do
|
|
create(:award_emoji, name: 'star', user: current_user, awardable: item1)
|
|
create(:award_emoji, :upvote, awardable: item1)
|
|
create(:award_emoji, :downvote, awardable: item1)
|
|
end
|
|
|
|
it 'executes limited number of N+1 queries', :use_sql_query_cache do
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create_list(:work_item, 2, project: project) do |item|
|
|
create(:award_emoji, name: 'rocket', awardable: item)
|
|
create_list(:award_emoji, 2, :upvote, awardable: item)
|
|
create_list(:award_emoji, 2, :downvote, awardable: item)
|
|
end
|
|
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_all_query_limit(control)
|
|
expect_graphql_errors_to_be_empty
|
|
end
|
|
end
|
|
|
|
context 'when fetching work item linked items widget' do
|
|
let_it_be(:other_project) { create(:project, :repository, :public, group: group) }
|
|
let_it_be(:other_milestone) { create(:milestone, project: other_project) }
|
|
let_it_be(:related_items) { create_list(:work_item, 3, project: other_project, milestone: other_milestone) }
|
|
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetLinkedItems {
|
|
linkedItems {
|
|
nodes {
|
|
linkId
|
|
linkType
|
|
linkCreatedAt
|
|
linkUpdatedAt
|
|
workItem {
|
|
id
|
|
widgets {
|
|
... on WorkItemWidgetMilestone {
|
|
milestone {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before do
|
|
create(:work_item_link, source: item1, target: related_items[0], link_type: 'relates_to')
|
|
create(:work_item_link, source: item2, target: related_items[0], link_type: 'relates_to')
|
|
end
|
|
|
|
it 'avoids N+1 queries', :use_sql_query_cache 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
|
|
|
|
item3 = create(:work_item, project: project, discussion_locked: true, title: 'item1', labels: [label1])
|
|
|
|
[item1, item2, item3].each do |item|
|
|
create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to')
|
|
create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to')
|
|
end
|
|
|
|
expect_graphql_errors_to_be_empty
|
|
expect { post_graphql(query, current_user: current_user) }
|
|
.not_to exceed_all_query_limit(control)
|
|
end
|
|
end
|
|
|
|
context 'when fetching work item participants widget' do
|
|
let_it_be(:other_project) { create(:project, group: group) }
|
|
let_it_be(:project) { other_project }
|
|
let_it_be(:users) { create_list(:user, 3) }
|
|
let_it_be(:work_items) { create_list(:work_item, 3, project: project, assignees: users) }
|
|
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetParticipants {
|
|
participants {
|
|
nodes {
|
|
id
|
|
username
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before do
|
|
project.add_guest(current_user)
|
|
end
|
|
|
|
it 'returns participants' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
participants_usernames = graphql_dig_at(items_data, 'widgets', 'participants', 'nodes', 'username')
|
|
expect(participants_usernames).to match_array(work_items.flat_map(&:participants).map(&:username))
|
|
end
|
|
|
|
it 'executes limited number of N+1 queries', :use_sql_query_cache do
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create_list(:work_item, 2, project: project, assignees: users)
|
|
|
|
expect_graphql_errors_to_be_empty
|
|
expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
|
|
end
|
|
end
|
|
|
|
context 'with development widget' do
|
|
context 'for closing merge requests field' do
|
|
let(:work_items) { [item1, item2] }
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
closingMergeRequests {
|
|
count
|
|
nodes {
|
|
id
|
|
fromMrDescription
|
|
mergeRequest { id }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
before do
|
|
work_items.each do |item|
|
|
create(
|
|
:merge_requests_closing_issues,
|
|
issue: item,
|
|
merge_request: create(:merge_request, source_project: project, target_branch: "feature#{item.id}")
|
|
)
|
|
end
|
|
end
|
|
|
|
it 'avoids N+1 queries' do
|
|
post_graphql(query, current_user: current_user) # warmup
|
|
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
expect(graphql_errors).to be_blank
|
|
|
|
2.times do
|
|
new_work_item = create(:work_item, project: project)
|
|
create(
|
|
:merge_requests_closing_issues,
|
|
issue: new_work_item,
|
|
merge_request: create(:merge_request, source_project: project, target_branch: "feature#{new_work_item.id}")
|
|
)
|
|
end
|
|
|
|
expect { post_graphql(query, current_user: current_user) }.to issue_same_number_of_queries_as(control)
|
|
expect(graphql_errors).to be_blank
|
|
end
|
|
end
|
|
|
|
context 'for related merge requests field' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
relatedMergeRequests {
|
|
nodes {
|
|
id
|
|
iid
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'limits the field to 1 execution' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect_graphql_errors_to_include(
|
|
'"relatedMergeRequests" field can be requested only for 1 WorkItemWidgetDevelopment(s) at a time.'
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'for related branches field' do
|
|
let(:fields) do
|
|
<<~GRAPHQL
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetDevelopment {
|
|
relatedBranches {
|
|
nodes {
|
|
name
|
|
comparePath
|
|
pipelineStatus { name label favicon }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
it 'limits the field to 1 execution' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect_graphql_errors_to_include(
|
|
'"relatedBranches" field can be requested only for 1 WorkItemWidgetDevelopment(s) at a time.'
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with error tracking widget' do
|
|
context "when we call the widget for many work items" do
|
|
let(:fields) do
|
|
<<~QUERY
|
|
nodes {
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetErrorTracking {
|
|
identifier
|
|
status
|
|
stackTrace {
|
|
nodes {
|
|
filename
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
QUERY
|
|
end
|
|
|
|
it 'returns the stack trace', :aggregate_failures do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect_graphql_errors_to_include(
|
|
"\"identifier\" field can be requested only for 1 WorkItemWidgetErrorTracking(s) at a time."
|
|
)
|
|
expect_graphql_errors_to_include(
|
|
"\"status\" field can be requested only for 1 WorkItemWidgetErrorTracking(s) at a time."
|
|
)
|
|
expect_graphql_errors_to_include(
|
|
"\"stackTrace\" field can be requested only for 1 WorkItemWidgetErrorTracking(s) at a time."
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when we call the widget for one work item" do
|
|
let(:widget_data) { graphql_data['workItem']['widgets'].find { |widget| widget['type'] == 'ERROR_TRACKING' } }
|
|
|
|
let(:latest_event) do
|
|
instance_double(Gitlab::ErrorTracking::ErrorEvent,
|
|
issue_id: sentry_issue.sentry_issue_identifier,
|
|
date_received: 1.day.ago,
|
|
stack_trace_entries: [
|
|
{
|
|
"filename" => "test.rb",
|
|
"lineNo" => 54,
|
|
"absPath" => "test.rb",
|
|
"function" => "<main>",
|
|
"colNo" => nil,
|
|
"context" => [[51, " end\n"], [52, "end\n"], [53, "\n"], [54, "yes"], [55, ""], [56, ""], [57, ""]]
|
|
}
|
|
]
|
|
)
|
|
end
|
|
|
|
let(:expected_response) do
|
|
{
|
|
"absolutePath" => "test.rb",
|
|
"columnNumber" => nil,
|
|
"context" => [
|
|
{ "line" => "<span id=\"LC1\" class=\"line\" lang=\"ruby\"> <span class=\"k\">end</span></span>",
|
|
"lineNumber" => 51 },
|
|
{ "line" => "<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"k\">end</span></span>",
|
|
"lineNumber" => 52 },
|
|
{ "line" => "<span id=\"LC1\" class=\"line\" lang=\"ruby\"></span>", "lineNumber" => 53 },
|
|
{ "line" => "<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"n\">yes</span></span>",
|
|
"lineNumber" => 54 },
|
|
{ "line" => "", "lineNumber" => 55 },
|
|
{ "line" => "", "lineNumber" => 56 },
|
|
{ "line" => "", "lineNumber" => 57 }
|
|
],
|
|
"filename" => "test.rb",
|
|
"function" => "<main>",
|
|
"lineNumber" => 54
|
|
}
|
|
end
|
|
|
|
let(:fields) do
|
|
<<~QUERY
|
|
id
|
|
widgets {
|
|
type
|
|
... on WorkItemWidgetErrorTracking {
|
|
identifier
|
|
status
|
|
stackTrace {
|
|
nodes {
|
|
filename
|
|
columnNumber
|
|
function
|
|
lineNumber
|
|
absolutePath
|
|
context {
|
|
line
|
|
lineNumber
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
QUERY
|
|
end
|
|
|
|
let(:query) do
|
|
graphql_query_for(
|
|
'work_item',
|
|
{ 'id' => item1.to_global_id.to_s },
|
|
fields
|
|
)
|
|
end
|
|
|
|
context "when sentry issue does not exist" do
|
|
let_it_be(:sentry_issue) { nil }
|
|
|
|
it 'returns not_found status and empty stack trace' do
|
|
post_graphql(query, current_user: current_user)
|
|
expect(widget_data.fetch("status")).to eq("NOT_FOUND")
|
|
expect(widget_data["stackTrace"]["nodes"]).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when sentry issue exists and the service is beings called" do
|
|
let_it_be(:sentry_issue) { create(:sentry_issue, issue: item1) }
|
|
|
|
before do
|
|
allow_next_instance_of(ErrorTracking::IssueLatestEventService) do |instance|
|
|
allow(instance).to receive(:execute).and_return(service_response)
|
|
end
|
|
end
|
|
|
|
context "when service reply with success" do
|
|
let(:service_response) { { status: :success, latest_event: latest_event } }
|
|
|
|
it 'returns the stack trace' do
|
|
post_graphql(query, current_user: current_user)
|
|
expect(widget_data.fetch("status")).to eq("SUCCESS")
|
|
expect(widget_data["stackTrace"]["nodes"].first).to match_array(expected_response)
|
|
end
|
|
end
|
|
|
|
context "when service reply with error with bad request" do
|
|
let(:service_response) { { status: :error, http_status: :bad_request } }
|
|
|
|
it 'returns error status and empty stack trace' do
|
|
post_graphql(query, current_user: current_user)
|
|
expect(widget_data.fetch("status")).to eq("ERROR")
|
|
expect(widget_data["stackTrace"]["nodes"]).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when service reply with error with no content" do
|
|
let(:service_response) { { status: :error, http_status: :no_content } }
|
|
|
|
it 'returns retry status and empty stack trace' do
|
|
post_graphql(query, current_user: current_user)
|
|
expect(widget_data.fetch("status")).to eq("RETRY")
|
|
expect(widget_data["stackTrace"]["nodes"]).to be_empty
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'filters' do
|
|
before_all do
|
|
project.add_developer(current_user)
|
|
end
|
|
|
|
let(:fields) do
|
|
<<~QUERY
|
|
nodes {
|
|
id
|
|
}
|
|
QUERY
|
|
end
|
|
|
|
context 'when filtering by confidential' do
|
|
let(:item_filter_params) { { confidential: true } }
|
|
|
|
it 'returns only confidential items' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(item_ids).to contain_exactly(confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when filtering by assignees' do
|
|
before_all do
|
|
IssueAssignee.create!(issue: item1, assignee: current_user)
|
|
IssueAssignee.create!(issue: item2, assignee: reporter)
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
let(:item_filter_params) { { assignee_usernames: [current_user.username] } }
|
|
|
|
it 'returns items assigned to the user' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { assignee_usernames: [current_user.username] } } }
|
|
|
|
it 'returns items not assigned to the user' do
|
|
expect(item_ids).to contain_exactly(item2.to_global_id.to_s, confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when using OR' do
|
|
let(:item_filter_params) { { or: { assignee_usernames: [current_user.username, reporter.username] } } }
|
|
|
|
it 'returns items assigned to either user' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s, item2.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when using a wildcard' do
|
|
let(:item_filter_params) { { assignee_wildcard_id: :NONE } }
|
|
|
|
it 'returns items without an assignee' do
|
|
expect(item_ids).to contain_exactly(confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when both assigneeUsernames and assigneeWildcardId are provided' do
|
|
let(:item_filter_params) { { assignee_usernames: [current_user.username], assignee_wildcard_id: :NONE } }
|
|
|
|
it 'returns an error' do
|
|
expect_graphql_errors_to_include(
|
|
'Only one of [assigneeUsernames, assigneeWildcardId] arguments is allowed at the same time.'
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by author' do
|
|
before_all do
|
|
item1.update_columns(author_id: current_user.id)
|
|
item2.update_columns(author_id: reporter.id)
|
|
confidential_item.update_columns(author_id: create(:user).id)
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
let(:item_filter_params) { { author_username: current_user.username } }
|
|
|
|
it 'returns items authored by the user' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { author_username: current_user.username } } }
|
|
|
|
it 'returns items not authored by the user' do
|
|
expect(item_ids).to contain_exactly(item2.to_global_id.to_s, confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when using OR' do
|
|
let(:item_filter_params) { { or: { author_usernames: [current_user.username, reporter.username] } } }
|
|
|
|
it 'returns items authored by either user' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s, item2.to_global_id.to_s)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by label' do
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
let(:item_filter_params) { { label_name: label1.name } }
|
|
|
|
it 'returns items with the label' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { label_name: label1.name } } }
|
|
|
|
it 'returns items without the label' do
|
|
expect(item_ids).to contain_exactly(item2.to_global_id.to_s, confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when using OR' do
|
|
let(:item_filter_params) { { or: { label_names: [label1.name, label2.name] } } }
|
|
|
|
it 'returns items authored by either user' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s, item2.to_global_id.to_s)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by milestone' do
|
|
before_all do
|
|
item1.update_columns(milestone_id: milestone1.id)
|
|
item2.update_columns(milestone_id: milestone2.id)
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
let(:item_filter_params) { { milestone_title: milestone1.title } }
|
|
|
|
it 'returns items with the milestone' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { milestone_title: milestone1.title } } }
|
|
|
|
it 'returns items without the milestone' do
|
|
expect(item_ids).to contain_exactly(item2.to_global_id.to_s, confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when using a wildcard' do
|
|
let(:item_filter_params) { { milestone_wildcard_id: :NONE } }
|
|
|
|
it 'returns items without a milestone' do
|
|
expect(item_ids).to contain_exactly(confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
|
|
context 'when both milestoneTitle and milestoneWildcardId are provided' do
|
|
let(:item_filter_params) { { milestone_title: [milestone1.title], milestone_wildcard_id: :NONE } }
|
|
|
|
it 'returns an error' do
|
|
expect_graphql_errors_to_include(
|
|
'Only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.'
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by reaction emoji' do
|
|
before_all do
|
|
create(:award_emoji, :upvote, user: current_user, awardable: item1)
|
|
create(:award_emoji, :downvote, user: current_user, awardable: item2)
|
|
end
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
let(:item_filter_params) { { my_reaction_emoji: AwardEmoji::THUMBS_UP } }
|
|
|
|
it 'returns items with the reaction emoji' do
|
|
expect(item_ids).to contain_exactly(item1.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { my_reaction_emoji: AwardEmoji::THUMBS_UP } } }
|
|
|
|
it 'returns items without the reaction emoji' do
|
|
expect(item_ids).to contain_exactly(item2.to_global_id.to_s, confidential_item.to_global_id.to_s)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when filtering by types' do
|
|
let(:item_filter_params) { { types: [:TASK] } }
|
|
|
|
let_it_be(:task) { create(:work_item, :task, project: project) }
|
|
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it 'returns items with selected types' do
|
|
expect(item_ids).to contain_exactly(task.to_global_id.to_s)
|
|
end
|
|
|
|
context 'when using NOT' do
|
|
let(:item_filter_params) { { not: { types: [:ISSUE] } } }
|
|
|
|
it 'returns items without selected types' do
|
|
expect(item_ids).to contain_exactly(task.to_global_id.to_s)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when skipping authorization' do
|
|
shared_examples 'request with skipped abilities' do |abilities = []|
|
|
it 'authorizes objects as expected' do
|
|
expect_any_instance_of(Gitlab::Graphql::Authorize::ObjectAuthorization) do |authorization|
|
|
expect(authorization).to receive(:ok).with(
|
|
project.work_items.first,
|
|
current_user,
|
|
scope_validator: nil,
|
|
skip_abilities: abilities
|
|
)
|
|
end
|
|
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
end
|
|
|
|
context 'when authorize_issue_types_in_finder feature flag is enabled' do
|
|
before do
|
|
stub_feature_flags(authorize_issue_types_in_finder: true)
|
|
end
|
|
|
|
it_behaves_like 'request with skipped abilities', [:read_work_item]
|
|
end
|
|
|
|
context 'when authorize_issue_types_in_finder feature flag is disabled' do
|
|
before do
|
|
stub_feature_flags(authorize_issue_types_in_finder: false)
|
|
end
|
|
|
|
it_behaves_like 'request with skipped abilities', []
|
|
end
|
|
end
|
|
|
|
def item_ids
|
|
graphql_dig_at(items_data, :id)
|
|
end
|
|
|
|
def query(params = item_filter_params)
|
|
graphql_query_for(
|
|
'project',
|
|
{ 'fullPath' => project.full_path },
|
|
query_graphql_field('workItems', params, fields)
|
|
)
|
|
end
|
|
end
|