diff options
Diffstat (limited to 'spec/graphql/resolvers')
18 files changed, 693 insertions, 99 deletions
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb index 8d2ae238bfe..d77a0b6242e 100644 --- a/spec/graphql/resolvers/base_resolver_spec.rb +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -7,8 +7,8 @@ RSpec.describe Resolvers::BaseResolver do let(:resolver) do Class.new(described_class) do - argument :test, ::GraphQL::INT_TYPE, required: false - type [::GraphQL::INT_TYPE], null: true + argument :test, ::GraphQL::Types::Int, required: false + type [::GraphQL::Types::Int], null: true def resolve(test: 100) process(object) @@ -22,7 +22,7 @@ RSpec.describe Resolvers::BaseResolver do let(:last_resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true def resolve(**args) [1, 2] @@ -36,11 +36,11 @@ RSpec.describe Resolvers::BaseResolver do context 'for a connection of scalars' do let(:resolver) do Class.new(described_class) do - type ::GraphQL::INT_TYPE.connection_type, null: true + type ::GraphQL::Types::Int.connection_type, null: true end end - it { is_expected.to eq(::GraphQL::INT_TYPE) } + it { is_expected.to eq(::GraphQL::Types::Int) } end context 'for a connection of objects' do @@ -64,21 +64,21 @@ RSpec.describe Resolvers::BaseResolver do context 'for a list type' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::STRING_TYPE], null: true + type [::GraphQL::Types::String], null: true end end - it { is_expected.to eq(::GraphQL::STRING_TYPE) } + it { is_expected.to eq(::GraphQL::Types::String) } end context 'for a scalar type' do let(:resolver) do Class.new(described_class) do - type ::GraphQL::BOOLEAN_TYPE, null: true + type ::GraphQL::Types::Boolean, null: true end end - it { is_expected.to eq(::GraphQL::BOOLEAN_TYPE) } + it { is_expected.to eq(::GraphQL::Types::Boolean) } end end @@ -88,7 +88,7 @@ RSpec.describe Resolvers::BaseResolver do end it 'has the correct (singular) type' do - expect(resolver.single.type).to eq(::GraphQL::INT_TYPE) + expect(resolver.single.type).to eq(::GraphQL::Types::Int) end it 'returns the same subclass every time' do @@ -105,10 +105,10 @@ RSpec.describe Resolvers::BaseResolver do describe '.when_single' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true when_single do - argument :foo, ::GraphQL::INT_TYPE, required: true + argument :foo, ::GraphQL::Types::Int, required: true end def resolve(foo: 1) @@ -138,14 +138,14 @@ RSpec.describe Resolvers::BaseResolver do context 'multiple when_single blocks' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::INT_TYPE], null: true + type [::GraphQL::Types::Int], null: true when_single do - argument :foo, ::GraphQL::INT_TYPE, required: true + argument :foo, ::GraphQL::Types::Int, required: true end when_single do - argument :bar, ::GraphQL::INT_TYPE, required: true + argument :bar, ::GraphQL::Types::Int, required: true end def resolve(foo: 1, bar: 2) @@ -168,7 +168,7 @@ RSpec.describe Resolvers::BaseResolver do let(:subclass) do Class.new(resolver) do when_single do - argument :inc, ::GraphQL::INT_TYPE, required: true + argument :inc, ::GraphQL::Types::Int, required: true end def resolve(foo:, inc:) @@ -194,7 +194,7 @@ RSpec.describe Resolvers::BaseResolver do context 'when the resolver returns early' do let(:resolver) do Class.new(described_class) do - type [::GraphQL::STRING_TYPE], null: true + type [::GraphQL::Types::String], null: true def ready?(**args) [false, %w[early return]] @@ -237,14 +237,14 @@ RSpec.describe Resolvers::BaseResolver do context 'when field is a connection' do it 'increases complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 1) expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3 expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7 end it 'does not increase complexity when filtering by iids' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6 expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3 diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb index 8d15d7eda1b..852aaf66201 100644 --- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ::CachingArrayResolver do Class.new(::Resolvers::BaseResolver) do include mod type [::Types::UserType], null: true - argument :is_admin, ::GraphQL::BOOLEAN_TYPE, required: false + argument :is_admin, ::GraphQL::Types::Boolean, required: false def query_input(is_admin:) is_admin @@ -50,7 +50,7 @@ RSpec.describe ::CachingArrayResolver do Class.new(::Resolvers::BaseResolver) do include mod type [::Types::UserType], null: true - argument :username, ::GraphQL::STRING_TYPE, required: false + argument :username, ::GraphQL::Types::String, required: false def query_input(username:) username diff --git a/spec/graphql/resolvers/concerns/resolves_ids_spec.rb b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb new file mode 100644 index 00000000000..1dd27c0eff0 --- /dev/null +++ b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResolvesIds do + # gid://gitlab/Project/6 + # gid://gitlab/Issue/6 + # gid://gitlab/Project/6 gid://gitlab/Issue/6 + context 'with a single project' do + let(:ids) { 'gid://gitlab/Project/6' } + let(:type) { ::Types::GlobalIDType[::Project] } + + it 'returns the correct array' do + expect(resolve_ids).to match_array(['6']) + end + end + + context 'with a single issue' do + let(:ids) { 'gid://gitlab/Issue/9' } + let(:type) { ::Types::GlobalIDType[::Issue] } + + it 'returns the correct array' do + expect(resolve_ids).to match_array(['9']) + end + end + + context 'with multiple users' do + let(:ids) { ['gid://gitlab/User/7', 'gid://gitlab/User/13', 'gid://gitlab/User/21'] } + let(:type) { ::Types::GlobalIDType[::User] } + + it 'returns the correct array' do + expect(resolve_ids).to match_array(%w[7 13 21]) + end + end + + def mock_resolver + Class.new(GraphQL::Schema::Resolver) { extend ResolvesIds } + end + + def resolve_ids + mock_resolver.resolve_ids(ids, type) + end +end diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb index 3dffda75e08..6f6855c8f84 100644 --- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb +++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb @@ -50,7 +50,7 @@ RSpec.describe ResolvesPipelines do end it 'increases field complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: false, max_page_size: 1) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: resolver, null: false, max_page_size: 1) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 2 expect(field.to_graphql.complexity.call({}, { sha: 'foo' }, 1)).to eq 4 diff --git a/spec/graphql/resolvers/echo_resolver_spec.rb b/spec/graphql/resolvers/echo_resolver_spec.rb index 4f48e5e0d7a..59a121ac7de 100644 --- a/spec/graphql/resolvers/echo_resolver_spec.rb +++ b/spec/graphql/resolvers/echo_resolver_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Resolvers::EchoResolver do let(:text) { 'Message test' } specify do - expect(described_class).to have_non_null_graphql_type(::GraphQL::STRING_TYPE) + expect(described_class).to have_non_null_graphql_type(::GraphQL::Types::String) end describe '#resolve' do diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb index bf8d2139c82..2aef483ac95 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do end context 'error matched' do - let(:detailed_error) { build(:detailed_error_tracking_error) } + let(:detailed_error) { build(:error_tracking_sentry_detailed_error) } before do allow(issue_details_service).to receive(:execute) diff --git a/spec/graphql/resolvers/groups_resolver_spec.rb b/spec/graphql/resolvers/groups_resolver_spec.rb new file mode 100644 index 00000000000..e53ca674163 --- /dev/null +++ b/spec/graphql/resolvers/groups_resolver_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::GroupsResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:group) { create(:group, name: 'public-group') } + let_it_be(:private_group) { create(:group, :private, name: 'private-group') } + let_it_be(:subgroup1) { create(:group, parent: group, name: 'Subgroup') } + let_it_be(:subgroup2) { create(:group, parent: subgroup1, name: 'Test Subgroup 2') } + let_it_be(:private_subgroup1) { create(:group, :private, parent: private_group, name: 'Subgroup1') } + let_it_be(:private_subgroup2) { create(:group, :private, parent: private_subgroup1, name: 'Subgroup2') } + let_it_be(:user) { create(:user) } + + before_all do + private_group.add_developer(user) + end + + shared_examples 'access to all public descendant groups' do + it 'returns all public descendant groups of the parent group ordered by ASC name' do + is_expected.to eq([subgroup1, subgroup2]) + end + end + + shared_examples 'access to all public subgroups' do + it 'returns all public subgroups of the parent group' do + is_expected.to contain_exactly(subgroup1) + end + end + + shared_examples 'returning empty results' do + it 'returns empty results' do + is_expected.to be_empty + end + end + + context 'when parent group is public' do + subject { resolve(described_class, obj: group, args: params, ctx: { current_user: current_user }) } + + context 'when `include_parent_descendants` is false' do + let(:params) { { include_parent_descendants: false } } + + context 'when user is not logged in' do + let(:current_user) { nil } + + it_behaves_like 'access to all public subgroups' + end + + context 'when user is logged in' do + let(:current_user) { user } + + it_behaves_like 'access to all public subgroups' + end + end + + context 'when `include_parent_descendants` is true' do + let(:params) { { include_parent_descendants: true } } + + context 'when user is not logged in' do + let(:current_user) { nil } + + it_behaves_like 'access to all public descendant groups' + end + + context 'when user is logged in' do + let(:current_user) { user } + + it_behaves_like 'access to all public descendant groups' + + context 'with owned argument set as true' do + before do + subgroup1.add_owner(current_user) + params[:owned] = true + end + + it 'returns only descendant groups owned by the user' do + is_expected.to contain_exactly(subgroup1) + end + end + + context 'with search argument' do + it 'returns only descendant groups with matching name or path' do + params[:search] = 'Test' + is_expected.to contain_exactly(subgroup2) + end + end + end + end + end + + context 'when parent group is private' do + subject { resolve(described_class, obj: private_group, args: params, ctx: { current_user: current_user }) } + + context 'when `include_parent_descendants` is true' do + let(:params) { { include_parent_descendants: true } } + + context 'when user is not logged in' do + let(:current_user) { nil } + + it_behaves_like 'returning empty results' + end + + context 'when user is logged in' do + let(:current_user) { user } + + it 'returns all private descendant groups' do + is_expected.to contain_exactly(private_subgroup1, private_subgroup2) + end + end + end + + context 'when `include_parent_descendants` is false' do + let(:params) { { include_parent_descendants: false } } + + context 'when user is not logged in' do + let(:current_user) { nil } + + it_behaves_like 'returning empty results' + end + + context 'when user is logged in' do + let(:current_user) { user } + + it 'returns private subgroups' do + is_expected.to contain_exactly(private_subgroup1) + end + end + end + end + end +end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 9b329e961cc..6e187e57729 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -11,9 +11,9 @@ RSpec.describe Resolvers::IssuesResolver do let_it_be(:project) { create(:project, group: group) } let_it_be(:other_project) { create(:project, group: group) } - let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:started_milestone) { create(:milestone, project: project, title: "started milestone", start_date: 1.day.ago) } let_it_be(:assignee) { create(:user) } - let_it_be(:issue1) { create(:incident, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) } + let_it_be(:issue1) { create(:incident, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: started_milestone) } let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) } let_it_be(:issue3) { create(:issue, project: other_project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) } let_it_be(:issue4) { create(:issue) } @@ -43,7 +43,63 @@ RSpec.describe Resolvers::IssuesResolver do end it 'filters by milestone' do - expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1) + expect(resolve_issues(milestone_title: [started_milestone.title])).to contain_exactly(issue1) + end + + describe 'filtering by milestone wildcard id' do + let_it_be(:upcoming_milestone) { create(:milestone, project: project, title: "upcoming milestone", start_date: 1.day.ago, due_date: 1.day.from_now) } + let_it_be(:past_milestone) { create(:milestone, project: project, title: "past milestone", due_date: 1.day.ago) } + let_it_be(:future_milestone) { create(:milestone, project: project, title: "future milestone", start_date: 1.day.from_now) } + let_it_be(:issue5) { create(:issue, project: project, state: :opened, milestone: upcoming_milestone) } + let_it_be(:issue6) { create(:issue, project: project, state: :opened, milestone: past_milestone) } + let_it_be(:issue7) { create(:issue, project: project, state: :opened, milestone: future_milestone) } + + let(:wildcard_started) { 'STARTED' } + let(:wildcard_upcoming) { 'UPCOMING' } + let(:wildcard_any) { 'ANY' } + let(:wildcard_none) { 'NONE' } + + it 'returns issues with started milestone' do + expect(resolve_issues(milestone_wildcard_id: wildcard_started)).to contain_exactly(issue1, issue5) + end + + it 'returns issues with upcoming milestone' do + expect(resolve_issues(milestone_wildcard_id: wildcard_upcoming)).to contain_exactly(issue5) + end + + it 'returns issues with any milestone' do + expect(resolve_issues(milestone_wildcard_id: wildcard_any)).to contain_exactly(issue1, issue5, issue6, issue7) + end + + it 'returns issues with no milestone' do + expect(resolve_issues(milestone_wildcard_id: wildcard_none)).to contain_exactly(issue2) + end + + it 'raises a mutually exclusive filter error when wildcard and title are provided' do + expect do + resolve_issues(milestone_title: ["started milestone"], milestone_wildcard_id: wildcard_started) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.') + end + + context 'negated filtering' do + it 'returns issues matching the searched title after applying a negated filter' do + expect(resolve_issues(milestone_title: ['past milestone'], not: { milestone_wildcard_id: wildcard_upcoming })).to contain_exactly(issue6) + end + + it 'returns issues excluding the ones with started milestone' do + expect(resolve_issues(not: { milestone_wildcard_id: wildcard_started })).to contain_exactly(issue7) + end + + it 'returns issues excluding the ones with upcoming milestone' do + expect(resolve_issues(not: { milestone_wildcard_id: wildcard_upcoming })).to contain_exactly(issue6) + end + + it 'raises a mutually exclusive filter error when wildcard and title are provided as negated filters' do + expect do + resolve_issues(not: { milestone_title: ["started milestone"], milestone_wildcard_id: wildcard_started }) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.') + end + end end it 'filters by two assignees' do @@ -169,7 +225,7 @@ RSpec.describe Resolvers::IssuesResolver do end it 'returns issues without the specified milestone' do - expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2) + expect(resolve_issues(not: { milestone_title: [started_milestone.title] })).to contain_exactly(issue2) end it 'returns issues without the specified assignee_usernames' do @@ -337,13 +393,13 @@ RSpec.describe Resolvers::IssuesResolver do end it 'finds a specific issue with iid', :request_store do - result = batch_sync(max_queries: 4) { resolve_issues(iid: issue1.iid).to_a } + result = batch_sync(max_queries: 5) { resolve_issues(iid: issue1.iid).to_a } expect(result).to contain_exactly(issue1) end it 'batches queries that only include IIDs', :request_store do - result = batch_sync(max_queries: 4) do + result = batch_sync(max_queries: 5) do [issue1, issue2] .map { |issue| resolve_issues(iid: issue.iid.to_s) } .flat_map(&:to_a) @@ -353,7 +409,7 @@ RSpec.describe Resolvers::IssuesResolver do end it 'finds a specific issue with iids', :request_store do - result = batch_sync(max_queries: 4) do + result = batch_sync(max_queries: 5) do resolve_issues(iids: [issue1.iid]).to_a end @@ -407,7 +463,7 @@ RSpec.describe Resolvers::IssuesResolver do end it 'increases field complexity based on arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4 expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8 diff --git a/spec/graphql/resolvers/merge_requests_count_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_count_resolver_spec.rb new file mode 100644 index 00000000000..da177da93a6 --- /dev/null +++ b/spec/graphql/resolvers/merge_requests_count_resolver_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::MergeRequestsCountResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:project1) { create(:project, :repository, :public) } + let_it_be(:project2) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) } + let_it_be(:issue) { create(:issue, project: project1) } + let_it_be(:merge_request_closing_issue1) { create(:merge_requests_closing_issues, issue: issue) } + let_it_be(:merge_request_closing_issue2) do + merge_request = create(:merge_request, source_project: project2) + create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) + end + + specify do + expect(described_class).to have_nullable_graphql_type(GraphQL::Types::Int) + end + + subject { batch_sync { resolve_merge_requests_count(issue) } } + + context "when user can only view an issue's closing merge requests that are public" do + it 'returns the count of the merge requests closing the issue' do + expect(subject).to eq(1) + end + end + + context "when user can view an issue's closing merge requests that are both public and private" do + before do + project2.add_reporter(user) + end + + it 'returns the count of the merge requests closing the issue' do + expect(subject).to eq(2) + end + end + end + + def resolve_merge_requests_count(obj) + resolve(described_class, obj: obj, ctx: { current_user: user }) + end +end diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index aec6c6c6708..64ee0d4f9cc 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -303,6 +303,29 @@ RSpec.describe Resolvers::MergeRequestsResolver do expect { resolve_mr(project, sort: :merged_at_desc, labels: %w[a b]) }.not_to raise_error end end + + context 'when sorting by closed at' do + before do + merge_request_1.metrics.update!(latest_closed_at: 10.days.ago) + merge_request_3.metrics.update!(latest_closed_at: 5.days.ago) + end + + it 'sorts merge requests ascending' do + expect(resolve_mr(project, sort: :closed_at_asc)) + .to match_array(mrs) + .and be_sorted(->(mr) { [closed_at(mr), -mr.id] }) + end + + it 'sorts merge requests descending' do + expect(resolve_mr(project, sort: :closed_at_desc)) + .to match_array(mrs) + .and be_sorted(->(mr) { [-closed_at(mr), -mr.id] }) + end + + def closed_at(mr) + nils_last(mr.metrics.latest_closed_at) + end + end end end end diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb index 618d012bd6d..b1f50a4a4a5 100644 --- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb @@ -145,7 +145,7 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do end it 'has an high complexity regardless of arguments' do - field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100) + field = Types::BaseField.new(name: 'test', type: GraphQL::Types::String.connection_type, resolver_class: described_class, null: false, max_page_size: 100) expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 24 expect(field.to_graphql.complexity.call({}, { include_subgroups: true }, 1)).to eq 24 diff --git a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb new file mode 100644 index 00000000000..82b05937aa3 --- /dev/null +++ b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::PaginatedTreeResolver do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository } + + specify do + expect(described_class).to have_nullable_graphql_type(Types::Tree::TreeType.connection_type) + end + + describe '#resolve', :aggregate_failures do + subject { resolve_repository(args, opts) } + + let(:args) { { ref: 'master' } } + let(:opts) { {} } + + let(:start_cursor) { subject.start_cursor } + let(:end_cursor) { subject.end_cursor } + let(:items) { subject.items } + let(:entries) { items.first.entries } + + it 'resolves to a collection with a tree object' do + expect(items.first).to be_an_instance_of(Tree) + + expect(start_cursor).to be_nil + expect(end_cursor).to be_blank + expect(entries.count).to eq(repository.tree.entries.count) + end + + context 'with recursive option' do + let(:args) { super().merge(recursive: true) } + + it 'resolve to a recursive tree' do + expect(entries[4].path).to eq('files/html') + end + end + + context 'with limited max_page_size' do + let(:opts) { { max_page_size: 5 } } + + it 'resolves to a pagination collection with a tree object' do + expect(items.first).to be_an_instance_of(Tree) + + expect(start_cursor).to be_nil + expect(end_cursor).to be_present + expect(entries.count).to eq(5) + end + end + + context 'when repository does not exist' do + before do + allow(repository).to receive(:exists?).and_return(false) + end + + it 'returns nil' do + is_expected.to be(nil) + end + end + + describe 'Cursor pagination' do + context 'when cursor is invalid' do + let(:args) { super().merge(after: 'invalid') } + + it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) } + end + + it 'returns all tree entries during cursor pagination' do + cursor = nil + + expected_entries = repository.tree.entries.map(&:path) + collected_entries = [] + + loop do + result = resolve_repository(args.merge(after: cursor), max_page_size: 10) + + collected_entries += result.items.first.entries.map(&:path) + + expect(result.start_cursor).to eq(cursor) + cursor = result.end_cursor + + break if cursor.blank? + end + + expect(collected_entries).to match_array(expected_entries) + end + end + end + + def resolve_repository(args, opts = {}) + field_options = described_class.field_options.merge( + owner: resolver_parent, + name: 'field_value' + ).merge(opts) + + field = ::Types::BaseField.new(**field_options) + resolve_field(field, repository, args: args, object_type: resolver_parent) + end +end diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb index 72a01b1c574..d0661c27b95 100644 --- a/spec/graphql/resolvers/project_resolver_spec.rb +++ b/spec/graphql/resolvers/project_resolver_spec.rb @@ -28,8 +28,8 @@ RSpec.describe Resolvers::ProjectResolver do end it 'does not increase complexity depending on number of load limits' do - field1 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100) - field2 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1) + field1 = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: described_class, null: false, max_page_size: 100) + field2 = Types::BaseField.new(name: 'test', type: GraphQL::Types::String, resolver_class: described_class, null: false, max_page_size: 1) expect(field1.to_graphql.complexity.call({}, {}, 1)).to eq 2 expect(field2.to_graphql.complexity.call({}, {}, 1)).to eq 2 diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb index 8c36153d485..75b9be7dfe7 100644 --- a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb @@ -86,11 +86,11 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver do context 'when Jira connection is not valid' do before do WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/project') - .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.'))) + .to_raise(JIRA::HTTPError.new(double(message: '{"errorMessages":["Some failure"]}'))) end it 'raises failure error' do - expect { resolve_jira_projects }.to raise_error('Jira request error: Some failure.') + expect { resolve_jira_projects }.to raise_error('An error occurred while requesting data from Jira: Some failure. Check your Jira integration configuration and try again.') end end end diff --git a/spec/graphql/resolvers/terraform/states_resolver_spec.rb b/spec/graphql/resolvers/terraform/states_resolver_spec.rb index 91d48cd782b..012c74ce398 100644 --- a/spec/graphql/resolvers/terraform/states_resolver_spec.rb +++ b/spec/graphql/resolvers/terraform/states_resolver_spec.rb @@ -43,7 +43,8 @@ RSpec.describe Resolvers::Terraform::StatesResolver.single do it do expect(subject).to be_present - expect(subject.type.to_s).to eq('String!') + expect(subject.type).to be_kind_of GraphQL::Schema::NonNull + expect(subject.type.unwrap).to eq GraphQL::Types::String expect(subject.description).to be_present end end diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb index bb4938c751f..f45f528fe7e 100644 --- a/spec/graphql/resolvers/timelog_resolver_spec.rb +++ b/spec/graphql/resolvers/timelog_resolver_spec.rb @@ -5,115 +5,306 @@ require 'spec_helper' RSpec.describe Resolvers::TimelogResolver do include GraphqlHelpers + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :empty_repo, :public, group: group) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError } + specify do expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type) end - context "with a group" do - let_it_be(:current_user) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, :empty_repo, :public, group: group) } + shared_examples_for 'with a project' do + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) } + let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.end_of_day) } + let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: 10.days.ago) } - describe '#resolve' do - let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day } - let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day } + let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } } - let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:merge_request) { create(:merge_request, source_project: project) } + it 'finds all timelogs within given dates' do + timelogs = resolve_timelogs(**args) - let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.beginning_of_day) } - let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.end_of_day) } - let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: medium_time_ago) } + expect(timelogs).to contain_exactly(timelog1) + end - let(:args) { { start_time: short_time_ago, end_time: short_time_ago.noon } } + context 'when no dates specified' do + let(:args) { {} } it 'finds all timelogs' do - timelogs = resolve_timelogs + timelogs = resolve_timelogs(**args) expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3) end + end - it 'finds all timelogs within given dates' do + context 'when only start_time present' do + let(:args) { { start_time: 2.days.ago.noon } } + + it 'finds timelogs after the start_time' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog2) + end + end + + context 'when only end_time present' do + let(:args) { { end_time: 2.days.ago.noon } } + + it 'finds timelogs before the end_time' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog3) + end + end + + context 'when start_time and end_date are present' do + let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } } + + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog2) + end + end + + context 'when start_date and end_time are present' do + let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } } + + it 'finds all timelogs within start_date and end_time' do timelogs = resolve_timelogs(**args) expect(timelogs).to contain_exactly(timelog1) end + end - context 'when only start_date is present' do - let(:args) { { start_date: short_time_ago } } + it 'return nothing when user has insufficient permissions' do + project2 = create(:project, :empty_repo, :private) + issue2 = create(:issue, project: project2) + create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.beginning_of_day) - it 'finds timelogs until the end of day of end_date' do - timelogs = resolve_timelogs(**args) + user = create(:user) - expect(timelogs).to contain_exactly(timelog1, timelog2) + expect(resolve_timelogs(user: user, obj: project2, **args)).to be_empty + end + + context 'when arguments are invalid' do + let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError } + + context 'when start_time and start_date are present' do + let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Provide either a start date or time, but not both/) end end - context 'when only end_date is present' do - let(:args) { { end_date: medium_time_ago } } + context 'when end_time and end_date are present' do + let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Provide either an end date or time, but not both/) + end + end - it 'finds timelogs until the end of day of end_date' do - timelogs = resolve_timelogs(**args) + context 'when start argument is after end argument' do + let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } } - expect(timelogs).to contain_exactly(timelog3) + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Start argument must be before End argument/) end end + end + end - context 'when start_time and end_date are present' do - let(:args) { { start_time: short_time_ago, end_date: short_time_ago } } + shared_examples "with a group" do + let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day } + let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day } - it 'finds timelogs until the end of day of end_date' do - timelogs = resolve_timelogs(**args) + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } - expect(timelogs).to contain_exactly(timelog1, timelog2) - end + let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.beginning_of_day) } + let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.end_of_day) } + let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: medium_time_ago) } + + let(:args) { { start_time: short_time_ago, end_time: short_time_ago.noon } } + + it 'finds all timelogs' do + timelogs = resolve_timelogs + + expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3) + end + + it 'finds all timelogs within given dates' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1) + end + + context 'when only start_date is present' do + let(:args) { { start_date: short_time_ago } } + + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog2) end + end - context 'when start_date and end_time are present' do - let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } } + context 'when only end_date is present' do + let(:args) { { end_date: medium_time_ago } } - it 'finds all timelogs within start_date and end_time' do - timelogs = resolve_timelogs(**args) + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) - expect(timelogs).to contain_exactly(timelog1) - end + expect(timelogs).to contain_exactly(timelog3) end + end - context 'when arguments are invalid' do - let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError } + context 'when start_time and end_date are present' do + let(:args) { { start_time: short_time_ago, end_date: short_time_ago } } - context 'when start_time and start_date are present' do - let(:args) { { start_time: short_time_ago, start_date: short_time_ago } } + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog2) + end + end + + context 'when start_date and end_time are present' do + let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } } + + it 'finds all timelogs within start_date and end_time' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1) + end + end + + context 'when arguments are invalid' do + context 'when start_time and start_date are present' do + let(:args) { { start_time: short_time_ago, start_date: short_time_ago } } - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Provide either a start date or time, but not both/) - end + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Provide either a start date or time, but not both/) end + end - context 'when end_time and end_date are present' do - let(:args) { { end_time: short_time_ago, end_date: short_time_ago } } + context 'when end_time and end_date are present' do + let(:args) { { end_time: short_time_ago, end_date: short_time_ago } } - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Provide either an end date or time, but not both/) - end + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Provide either an end date or time, but not both/) end + end - context 'when start argument is after end argument' do - let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } } + context 'when start argument is after end argument' do + let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } } - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Start argument must be before End argument/) - end + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Start argument must be before End argument/) end end end end - def resolve_timelogs(user: current_user, **args) + shared_examples "with a user" do + let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day } + let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day } + + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let_it_be(:timelog1) { create(:issue_timelog, issue: issue, user: current_user) } + let_it_be(:timelog2) { create(:issue_timelog, issue: issue, user: create(:user)) } + let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, user: current_user) } + + it 'blah' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog3) + end + end + + context "on a project" do + let(:object) { project } + let(:extra_args) { {} } + + it_behaves_like 'with a project' + end + + context "with a project filter" do + let(:object) { nil } + let(:extra_args) { { project_id: project.to_global_id } } + + it_behaves_like 'with a project' + end + + context 'on a group' do + let(:object) { group } + let(:extra_args) { {} } + + it_behaves_like 'with a group' + end + + context 'with a group filter' do + let(:object) { nil } + let(:extra_args) { { group_id: group.to_global_id } } + + it_behaves_like 'with a group' + end + + context 'on a user' do + let(:object) { current_user } + let(:extra_args) { {} } + let(:args) { {} } + + it_behaves_like 'with a user' + end + + context 'with a user filter' do + let(:object) { nil } + let(:extra_args) { { username: current_user.username } } + let(:args) { {} } + + it_behaves_like 'with a user' + end + + context 'when > `default_max_page_size` records' do + let(:object) { nil } + let!(:timelog_list) { create_list(:timelog, 101, issue: issue) } + let(:args) { { project_id: "gid://gitlab/Project/#{project.id}" } } + let(:extra_args) { {} } + + it 'pagination returns `default_max_page_size` and sets `has_next_page` true' do + timelogs = resolve_timelogs(**args) + + expect(timelogs.items.count).to be(100) + expect(timelogs.has_next_page).to be(true) + end + end + + context 'when no object or arguments provided' do + let(:object) { nil } + let(:args) { {} } + let(:extra_args) { {} } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Provide at least one argument/) + end + end + + def resolve_timelogs(user: current_user, obj: object, **args) context = { current_user: user } - resolve(described_class, obj: group, args: args, ctx: context) + resolve(described_class, obj: obj, args: args.merge(extra_args), ctx: context) end end diff --git a/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb b/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb index cc855bbcb53..70f06b58a65 100644 --- a/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb +++ b/spec/graphql/resolvers/user_discussions_count_resolver_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Resolvers::UserDiscussionsCountResolver do let_it_be(:private_discussion) { create_list(:discussion_note_on_issue, 3, noteable: private_issue, project: private_project) } specify do - expect(described_class).to have_nullable_graphql_type(GraphQL::INT_TYPE) + expect(described_class).to have_nullable_graphql_type(GraphQL::Types::Int) end context 'when counting discussions from a public issue' do diff --git a/spec/graphql/resolvers/user_notes_count_resolver_spec.rb b/spec/graphql/resolvers/user_notes_count_resolver_spec.rb index 6cf23a2f57f..bc173b2a166 100644 --- a/spec/graphql/resolvers/user_notes_count_resolver_spec.rb +++ b/spec/graphql/resolvers/user_notes_count_resolver_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Resolvers::UserNotesCountResolver do let_it_be(:private_project) { create(:project, :repository, :private) } specify do - expect(described_class).to have_nullable_graphql_type(GraphQL::INT_TYPE) + expect(described_class).to have_nullable_graphql_type(GraphQL::Types::Int) end context 'when counting notes from an issue' do |