Commit 73386287 authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Eulyeon Ko

Add confidential issues filtering for GraphQL API

"confidential" argument (boolean) is added
to the issues resolver to allow for filtering
by confidentiality state.

By default, the filter is false and returns all issues.
Setting the filter true filters for confidential issues.

Adds API request specs for group issues GraphQL API
(we didn't have coverage previously).

Changelog: added
parent b51e7099
...@@ -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.'
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 },
......
...@@ -10300,6 +10300,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -10300,6 +10300,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. |
| <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. |
...@@ -12735,6 +12736,7 @@ Returns [`Issue`](#issue). ...@@ -12735,6 +12736,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. |
| <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. |
...@@ -12771,6 +12773,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). ...@@ -12771,6 +12773,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. |
| <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". |
...@@ -12805,6 +12808,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -12805,6 +12808,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. |
| <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,30 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -222,6 +229,30 @@ RSpec.describe Resolvers::IssuesResolver do
end end
end end
context 'filtering for confidential issues' do
include_context 'filtering for confidential issues'
context 'when filter is explicitly set to false' do
it 'returns all viewable issues' do
expect(resolve_issues).to contain_exactly(issue1, issue2, confidential_issue1)
end
end
context "when user is allowed to view confidential issues" do
it "returns only the confidential issues of the project" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1)
end
end
context "when user is not allowed to see confidential issues" do
it 'does not return the confidential issues' do
project.add_guest(current_user)
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 +550,56 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -519,32 +550,56 @@ 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 'filtering for confidential issues' do
include_context 'filtering for confidential issues'
context 'when filter is explicitly set to false' do
it 'returns all viewable issues' do
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
end
end
context "when user is allowed to view confidential issues" do
it "returns only the confidential issues for the group" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
end
end
context "when user is not allowed to see confidential issues" do
it 'does not return the confidential issues' do
group.add_guest(current_user)
expect(resolve_issues({ confidential: true })).to be_empty
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 +620,6 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -565,6 +620,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
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
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,24 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -159,12 +160,24 @@ 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
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 +188,16 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -175,6 +188,16 @@ 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
end end
end end
...@@ -526,4 +549,8 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -526,4 +549,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