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
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
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,
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
......
......@@ -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="groupissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after 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="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. |
......@@ -12735,6 +12736,7 @@ Returns [`Issue`](#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="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="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. |
......@@ -12771,6 +12773,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <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="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="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <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):
| <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="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="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. |
......
......@@ -26,7 +26,14 @@ RSpec.describe Resolvers::IssuesResolver do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
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
let!(:obj) { project }
before_all do
project.add_developer(current_user)
project.add_reporter(reporter)
......@@ -222,6 +229,30 @@ RSpec.describe Resolvers::IssuesResolver do
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
it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
......@@ -519,32 +550,56 @@ RSpec.describe Resolvers::IssuesResolver do
end
context "with a group" do
let!(:obj) { group }
before do
group.add_developer(current_user)
end
describe '#resolve' do
it 'finds all group issues' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user })
expect(result).to contain_exactly(issue1, issue2, issue3)
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
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
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, _|
loader.call("non-existent-path", nil)
end
end
let!(:obj) { project }
it "returns nil without breaking" do
expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
......@@ -565,6 +620,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
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
# 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
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
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(:issue_filter_params) { {} }
......@@ -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(: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
'thumbsup' | lazy { [issue_a_gid] }
'ANY' | lazy { [issue_a_gid] }
......@@ -84,7 +83,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
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
......@@ -149,6 +148,8 @@ RSpec.describe 'getting an issue list for a project' do
create(:issue, :confidential, project: project)
end
let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s }
context 'when the user cannot see confidential issues' do
it 'returns issues without confidential issues' do
post_graphql(query, current_user: current_user)
......@@ -159,12 +160,24 @@ RSpec.describe 'getting an issue list for a project' do
expect(issue.dig('node', 'confidential')).to eq(false)
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
context 'when the user can see confidential issues' do
it 'returns issues with confidential issues' do
before do
project.add_developer(current_user)
end
it 'returns issues with confidential issues' do
post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(3)
......@@ -175,6 +188,16 @@ RSpec.describe 'getting an issue list for a project' do
expect(confidentials).to eq([true, false, false])
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
......@@ -526,4 +549,8 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check'
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
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