gitlab-ce/spec/requests/api/graphql/project/work_items_spec.rb

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