Commit 228e7ddd authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '327344-allow-filtering-of-issues-by-confidentiality-in-graphql' into 'master'

Add filtering for confidential issues for GraphQL API

See merge request gitlab-org/gitlab!71355
parents a5391f65 821c107a
...@@ -60,6 +60,10 @@ module IssueResolverArguments ...@@ -60,6 +60,10 @@ module IssueResolverArguments
argument :my_reaction_emoji, GraphQL::Types::String, argument :my_reaction_emoji, GraphQL::Types::String,
required: false, required: false,
description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.' description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
argument :confidential,
GraphQL::Types::Boolean,
required: false,
description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
argument :not, Types::Issues::NegatedIssueFilterInputType, argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.', description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h }, prepare: ->(negated_args, ctx) { negated_args.to_h },
......
...@@ -10303,6 +10303,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -10303,6 +10303,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | | <a id="groupissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="groupissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. | | <a id="groupissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="groupissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. | | <a id="groupissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="groupissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. | | <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
...@@ -12738,6 +12739,7 @@ Returns [`Issue`](#issue). ...@@ -12738,6 +12739,7 @@ Returns [`Issue`](#issue).
| <a id="projectissueauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | | <a id="projectissueauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissueclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. | | <a id="projectissueclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissueclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. | | <a id="projectissueclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissueconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. | | <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
...@@ -12774,6 +12776,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). ...@@ -12774,6 +12776,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | | <a id="projectissuestatuscountsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissuestatuscountsclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. | | <a id="projectissuestatuscountsclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissuestatuscountsclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. | | <a id="projectissuestatuscountsclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissuestatuscountsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. | | <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". | | <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
...@@ -12808,6 +12811,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -12808,6 +12811,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | | <a id="projectissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. | | <a id="projectissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. | | <a id="projectissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. | | <a id="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. | | <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | | <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
......
...@@ -26,7 +26,14 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -26,7 +26,14 @@ RSpec.describe Resolvers::IssuesResolver do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type) expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
end end
shared_context 'filtering for confidential issues' do
let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
end
context "with a project" do context "with a project" do
let(:obj) { project }
before_all do before_all do
project.add_developer(current_user) project.add_developer(current_user)
project.add_reporter(reporter) project.add_reporter(reporter)
...@@ -222,6 +229,42 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -222,6 +229,42 @@ RSpec.describe Resolvers::IssuesResolver do
end end
end end
context 'confidential issues' do
include_context 'filtering for confidential issues'
context "when user is allowed to view confidential issues" do
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, confidential_issue1)
end
it 'returns only the non-confidential issues for the project when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
end
it "returns only the confidential issues for the project when filter is set to true" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1)
end
end
context "when user is not allowed to see confidential issues" do
before do
project.add_guest(current_user)
end
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2)
end
it 'does not return the confidential issues when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
end
it 'does not return the confidential issues when filter is set to true' do
expect(resolve_issues({ confidential: true })).to be_empty
end
end
end
context 'when searching issues' do context 'when searching issues' do
it 'returns correct issues' do it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
...@@ -519,32 +562,72 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -519,32 +562,72 @@ RSpec.describe Resolvers::IssuesResolver do
end end
context "with a group" do context "with a group" do
let(:obj) { group }
before do before do
group.add_developer(current_user) group.add_developer(current_user)
end end
describe '#resolve' do describe '#resolve' do
it 'finds all group issues' do it 'finds all group issues' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user }) expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
expect(result).to contain_exactly(issue1, issue2, issue3)
end end
it 'returns issues without the specified issue_type' do it 'returns issues without the specified issue_type' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user }, args: { not: { types: ['issue'] } }) expect(resolve_issues({ not: { types: ['issue'] } })).to contain_exactly(issue1)
end
expect(result).to contain_exactly(issue1) context "confidential issues" do
include_context 'filtering for confidential issues'
context "when user is allowed to view confidential issues" do
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
end
context 'filtering for confidential issues' do
it 'returns only the non-confidential issues for the group when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
end
it "returns only the confidential issues for the group when filter is set to true" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
end
end
end
context "when user is not allowed to see confidential issues" do
before do
group.add_guest(current_user)
end
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
context 'filtering for confidential issues' do
it 'does not return the confidential issues when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
end
it 'does not return the confidential issues when filter is set to true' do
expect(resolve_issues({ confidential: true })).to be_empty
end
end
end
end end
end end
end end
context "when passing a non existent, batch loaded project" do context "when passing a non existent, batch loaded project" do
let(:project) do let!(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _| BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|
loader.call("non-existent-path", nil) loader.call("non-existent-path", nil)
end end
end end
let(:obj) { project }
it "returns nil without breaking" do it "returns nil without breaking" do
expect(resolve_issues(iids: ["don't", "break"])).to be_empty expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end end
...@@ -565,6 +648,6 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -565,6 +648,6 @@ RSpec.describe Resolvers::IssuesResolver do
end end
def resolve_issues(args = {}, context = { current_user: current_user }) def resolve_issues(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context) resolve(described_class, obj: obj, args: args, ctx: context)
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting an issue list for a group' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:project1) { create(:project, :public, group: group1) }
let_it_be(:project2) { create(:project, :private, group: group1) }
let_it_be(:project3) { create(:project, :public, group: group2) }
let_it_be(:issue1) { create(:issue, project: project1) }
let_it_be(:issue2) { create(:issue, project: project2) }
let_it_be(:issue3) { create(:issue, project: project3) }
let(:issue1_gid) { issue1.to_global_id.to_s }
let(:issue2_gid) { issue2.to_global_id.to_s }
let(:issues_data) { graphql_data['group']['issues']['edges'] }
let(:issue_filter_params) { {} }
let(:fields) do
<<~QUERY
edges {
node {
#{all_graphql_fields_for('issues'.classify)}
}
}
QUERY
end
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group1.full_path },
query_graphql_field('issues', issue_filter_params, fields)
)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when there is a confidential issue' do
let_it_be(:confidential_issue1) { create(:issue, :confidential, project: project1) }
let_it_be(:confidential_issue2) { create(:issue, :confidential, project: project2) }
let_it_be(:confidential_issue3) { create(:issue, :confidential, project: project3) }
let(:confidential_issue1_gid) { confidential_issue1.to_global_id.to_s }
let(:confidential_issue2_gid) { confidential_issue2.to_global_id.to_s }
context 'when the user cannot see confidential issues' do
before do
group1.add_guest(current_user)
end
it 'returns issues without confidential issues for the group' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns no issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to be_empty
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
end
end
context 'when the user can see confidential issues' do
before do
group1.add_developer(current_user)
end
it 'returns issues with confidential issues for the group' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid, confidential_issue1_gid, confidential_issue2_gid)
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(confidential_issue1_gid, confidential_issue2_gid)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
end
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
end
...@@ -11,6 +11,8 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -11,6 +11,8 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) } let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] } let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:issue_a_gid) { issue_a.to_global_id.to_s }
let(:issue_b_gid) { issue_b.to_global_id.to_s }
let(:issues_data) { graphql_data['project']['issues']['edges'] } let(:issues_data) { graphql_data['project']['issues']['edges'] }
let(:issue_filter_params) { {} } let(:issue_filter_params) { {} }
...@@ -66,9 +68,6 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -66,9 +68,6 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) } let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) }
let(:issue_a_gid) { issue_a.to_global_id.to_s }
let(:issue_b_gid) { issue_b.to_global_id.to_s }
where(:value, :gids) do where(:value, :gids) do
'thumbsup' | lazy { [issue_a_gid] } 'thumbsup' | lazy { [issue_a_gid] }
'ANY' | lazy { [issue_a_gid] } 'ANY' | lazy { [issue_a_gid] }
...@@ -84,7 +83,7 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -84,7 +83,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids) expect(issues_ids).to eq(gids)
end end
end end
end end
...@@ -149,6 +148,8 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -149,6 +148,8 @@ RSpec.describe 'getting an issue list for a project' do
create(:issue, :confidential, project: project) create(:issue, :confidential, project: project)
end end
let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s }
context 'when the user cannot see confidential issues' do context 'when the user cannot see confidential issues' do
it 'returns issues without confidential issues' do it 'returns issues without confidential issues' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
...@@ -159,12 +160,34 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -159,12 +160,34 @@ RSpec.describe 'getting an issue list for a project' do
expect(issue.dig('node', 'confidential')).to eq(false) expect(issue.dig('node', 'confidential')).to eq(false)
end end
end end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns no issues' do
post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(0)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
end
end
end end
context 'when the user can see confidential issues' do context 'when the user can see confidential issues' do
it 'returns issues with confidential issues' do before do
project.add_developer(current_user) project.add_developer(current_user)
end
it 'returns issues with confidential issues' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(3) expect(issues_data.size).to eq(3)
...@@ -175,6 +198,26 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -175,6 +198,26 @@ RSpec.describe 'getting an issue list for a project' do
expect(confidentials).to eq([true, false, false]) expect(confidentials).to eq([true, false, false])
end end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(confidential_issue_gid)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
end
end
end end
end end
...@@ -526,4 +569,8 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -526,4 +569,8 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check' include_examples 'N+1 query check'
end end
end end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment