diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 23:50:22 +0000 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /spec/graphql | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) | |
download | gitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'spec/graphql')
64 files changed, 1933 insertions, 256 deletions
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb index 33b11e1ca09..64e423e2bf8 100644 --- a/spec/graphql/features/authorization_spec.rb +++ b/spec/graphql/features/authorization_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Gitlab::Graphql::Authorize' do +RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do include GraphqlHelpers include Graphql::ResolverFactories @@ -10,10 +10,14 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:permission_single) { :foo } let(:permission_collection) { [:foo, :bar] } let(:test_object) { double(name: 'My name') } + let(:authorizing_object) { test_object } + # to override when combining permissions + let(:permission_object_one) { authorizing_object } + let(:permission_object_two) { authorizing_object } + let(:query_string) { '{ item { name } }' } let(:result) do schema = empty_schema - schema.use(Gitlab::Graphql::Authorize) execute_query(query_type, schema: schema) end @@ -33,18 +37,25 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do shared_examples 'authorization with a collection of permissions' do it 'returns the protected field when user has all permissions' do - permit(*permission_collection) + permit_on(permission_object_one, permission_collection.first) + permit_on(permission_object_two, permission_collection.second) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user only has one of the permissions' do - permit(permission_collection.first) + permit_on(permission_object_one, permission_collection.first) expect(subject).to be_nil end - it 'returns nil when user only has none of the permissions' do + it 'returns nil when user only has the other of the permissions' do + permit_on(permission_object_two, permission_collection.second) + + expect(subject).to be_nil + end + + it 'returns nil when user has neither of the required permissions' do expect(subject).to be_nil end end @@ -56,6 +67,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do describe 'Field authorizations' do let(:type) { type_factory } + let(:authorizing_object) { nil } describe 'with a single permission' do let(:query_type) do @@ -71,9 +83,10 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:query_type) do permissions = permission_collection query_factory do |qt| - qt.field :item, type, null: true, resolver: new_resolver(test_object) do - authorize permissions - end + qt.field :item, type, + null: true, + resolver: new_resolver(test_object), + authorize: permissions end end @@ -110,9 +123,9 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:type) do permissions = permission_collection type_factory do |type| - type.field :name, GraphQL::STRING_TYPE, null: true do - authorize permissions - end + type.field :name, GraphQL::STRING_TYPE, + null: true, + authorize: permissions end end @@ -163,6 +176,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do end describe 'type and field authorizations together' do + let(:authorizing_object) { anything } let(:permission_1) { permission_collection.first } let(:permission_2) { permission_collection.last } @@ -181,7 +195,63 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do include_examples 'authorization with a collection of permissions' end - describe 'type authorizations when applied to a relay connection' do + describe 'resolver and field authorizations together' do + let(:permission_1) { permission_collection.first } + let(:permission_2) { permission_collection.last } + let(:type) { type_factory } + + let(:query_type) do + query_factory do |query| + query.field :item, type, + null: true, + resolver: resolver, + authorize: permission_2 + end + end + + context 'when the resolver authorizes the object' do + let(:permission_object_one) { be_nil } + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = simple_resolver(test_object) + resolver.include(::Gitlab::Graphql::Authorize::AuthorizeResource) + resolver.authorize permission_1 + resolver.authorizes_object! + resolver + end + + include_examples 'authorization with a collection of permissions' + end + + context 'when the resolver does not authorize the object, but instead calls authorized_find!' do + let(:permission_object_one) { test_object } + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = new_resolver(test_object, method: :find_object) + resolver.authorize permission_1 + resolver + end + + include_examples 'authorization with a collection of permissions' + end + + context 'when the resolver calls authorized_find!, but does not list any permissions' do + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = new_resolver(test_object, method: :find_object) + resolver + end + + it 'raises a configuration error' do + permit_on(permission_object_two, permission_collection.second) + + expect { execute_query(query_type) } + .to raise_error(::Gitlab::Graphql::Authorize::AuthorizeResource::ConfigurationError) + end + end + end + + describe 'when type authorizations when applied to a relay connection' do let(:query_string) { '{ item { edges { node { name } } } }' } let(:second_test_object) { double(name: 'Second thing') } @@ -220,8 +290,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' } it 'only checks permissions for the first object' do - expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true } - expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object) + expect(Ability) + .to receive(:allowed?) + .with(user, permission_single, test_object) + .and_return(true) + expect(Ability) + .not_to receive(:allowed?).with(user, permission_single, second_test_object) expect(subject.size).to eq(1) end @@ -262,10 +336,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do end let(:project_type) do |type| + issues = Issue.where(project: [visible_project, other_project]).order(id: :asc) type_factory do |type| type.graphql_name 'FakeProjectType' - type.field :test_issues, issue_type.connection_type, null: false, - resolver: new_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc)) + type.field :test_issues, issue_type.connection_type, + null: false, + resolver: new_resolver(issues) end end @@ -300,11 +376,35 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do end end + describe 'Authorization on GraphQL::Execution::Execute::SKIP' do + let(:type) do + type_factory do |type| + type.authorize permission_single + end + end + + let(:query_type) do + query_factory do |query| + query.field :item, [type], null: true, resolver: new_resolver(GraphQL::Execution::Execute::SKIP) + end + end + + it 'skips redaction' do + expect(Ability).not_to receive(:allowed?) + + result + end + end + private def permit(*permissions) + permit_on(authorizing_object, *permissions) + end + + def permit_on(object, *permissions) permissions.each do |permission| - allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true) + allow(Ability).to receive(:allowed?).with(user, permission, object).and_return(true) end end end diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index cb2bb25b098..1f2c518f83c 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -14,10 +14,6 @@ RSpec.describe GitlabSchema do expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::GenericTracing)) end - it 'enables the authorization instrumenter' do - expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation)) - end - it 'has the base mutation' do expect(described_class.mutation).to eq(::Types::MutationType) end @@ -210,18 +206,22 @@ RSpec.describe GitlabSchema do describe '.parse_gid' do let_it_be(:global_id) { 'gid://gitlab/TestOne/2147483647' } + subject(:parse_gid) { described_class.parse_gid(global_id) } + before do test_base = Class.new test_one = Class.new(test_base) test_two = Class.new(test_base) + test_three = Class.new(test_base) stub_const('TestBase', test_base) stub_const('TestOne', test_one) stub_const('TestTwo', test_two) + stub_const('TestThree', test_three) end it 'parses the gid' do - gid = described_class.parse_gid(global_id) + gid = parse_gid expect(gid.model_id).to eq '2147483647' expect(gid.model_class).to eq TestOne @@ -231,7 +231,7 @@ RSpec.describe GitlabSchema do let_it_be(:global_id) { 'malformed://gitlab/TestOne/2147483647' } it 'raises an error' do - expect { described_class.parse_gid(global_id) } + expect { parse_gid } .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID.") end end @@ -253,6 +253,33 @@ RSpec.describe GitlabSchema do expect { described_class.parse_gid(global_id, expected_type: TestTwo) } .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestTwo.") end + + context 'when expected_type is an array' do + subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) } + + context 'when global_id is of type TestOne' do + it 'returns an object of an expected type' do + expect(parse_gid.model_class).to eq TestOne + end + end + + context 'when global_id is of type TestTwo' do + let_it_be(:global_id) { 'gid://gitlab/TestTwo/2147483647' } + + it 'returns an object of an expected type' do + expect(parse_gid.model_class).to eq TestTwo + end + end + + context 'when global_id is of type TestThree' do + let_it_be(:global_id) { 'gid://gitlab/TestThree/2147483647' } + + it 'rejects an unknown type' do + expect { parse_gid } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestOne, TestTwo.") + end + end + end end end diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb index 24104a20465..dd9305d2197 100644 --- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::Boards::Issues::IssueMoveList do + include GraphqlHelpers + let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, group: group) } let_it_be(:board) { create(:board, group: group) } @@ -16,9 +18,8 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) } let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) } - let(:current_user) { user } - let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } - let(:params) { { board: board, project_path: project.full_path, iid: issue1.iid } } + let(:current_ctx) { { current_user: user } } + let(:params) { { board_id: global_id_of(board), project_path: project.full_path, iid: issue1.iid } } let(:move_params) do { from_list_id: list1.id, @@ -33,26 +34,45 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do group.add_guest(guest) end - subject do - mutation.resolve(**params.merge(move_params)) - end + describe '#resolve' do + subject do + sync(resolve(described_class, args: params.merge(move_params), ctx: current_ctx)) + end + + %i[from_list_id to_list_id].each do |arg_name| + context "when we only pass #{arg_name}" do + let(:move_params) { { arg_name => list1.id } } - describe '#ready?' do - it 'raises an error if required arguments are missing' do - expect { mutation.ready?(**params) } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \ - "fromListId, toListId, afterId or beforeId is required") + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ArgumentError, + 'Both fromListId and toListId must be present' + ) + end + end end - it 'raises an error if only one of fromListId and toListId is present' do - expect { mutation.ready?(**params.merge(from_list_id: list1.id)) } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, - 'Both fromListId and toListId must be present' + context 'when required arguments are missing' do + let(:move_params) { {} } + + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ArgumentError, + "At least one of the arguments fromListId, toListId, afterId or beforeId is required" ) + end + end + + context 'when the board ID is wrong' do + before do + params[:board_id] = global_id_of(project) + end + + it 'raises an error' do + expect { subject }.to raise_error(::GraphQL::LoadApplicationObjectFailedError) + end end - end - describe '#resolve' do context 'when user have access to resources' do it 'moves and repositions issue' do subject @@ -63,15 +83,11 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do end end - context 'when user have no access to resources' do - shared_examples 'raises a resource not available error' do - it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } - end - - context 'when user cannot update issue' do - let(:current_user) { guest } + context 'when user cannot update issue' do + let(:current_ctx) { { current_user: guest } } - it_behaves_like 'raises a resource not available error' + specify do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end end diff --git a/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb b/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb deleted file mode 100644 index 8d1fce406fa..00000000000 --- a/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Mutations::CanMutateSpammable do - let(:mutation_class) do - Class.new(Mutations::BaseMutation) do - include Mutations::CanMutateSpammable - end - end - - let(:request) { double(:request) } - let(:query) { double(:query, schema: GitlabSchema) } - let(:context) { GraphQL::Query::Context.new(query: query, object: nil, values: { request: request }) } - - subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) } - - describe '#additional_spam_params' do - it 'returns additional spam-related params' do - expect(subject.send(:additional_spam_params)).to eq({ api: true, request: request }) - end - end - - describe '#with_spam_action_fields' do - let(:spam_log) { double(:spam_log, id: 1) } - let(:spammable) { double(:spammable, spam?: true, render_recaptcha?: true, spam_log: spam_log) } - - before do - allow(Gitlab::CurrentSettings).to receive(:recaptcha_site_key) { 'abc123' } - end - - it 'merges in spam action fields from spammable' do - result = subject.send(:with_spam_action_response_fields, spammable) do - { other_field: true } - end - expect(result) - .to eq({ - spam: true, - needs_captcha_response: true, - spam_log_id: 1, - captcha_site_key: 'abc123', - other_field: true - }) - end - end -end diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb index 326d88cea80..ada88b7652c 100644 --- a/spec/graphql/mutations/design_management/upload_spec.rb +++ b/spec/graphql/mutations/design_management/upload_spec.rb @@ -32,6 +32,10 @@ RSpec.describe Mutations::DesignManagement::Upload do end context "when the feature is not available" do + before do + enable_design_management(false) + end + it_behaves_like "resource not available" end @@ -52,10 +56,10 @@ RSpec.describe Mutations::DesignManagement::Upload do .map { |f| RenameableUpload.unique_file(f) } end - def creates_designs + def creates_designs(&block) prior_count = DesignManagement::Design.count - expect { yield }.not_to raise_error + expect(&block).not_to raise_error expect(DesignManagement::Design.count).to eq(prior_count + files.size) end @@ -99,20 +103,20 @@ RSpec.describe Mutations::DesignManagement::Upload do it_behaves_like "resource not available" end - context "a valid design" do + context "with a valid design" do it "returns the updated designs" do expect(resolve[:errors]).to eq [] expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png") end end - context "context when passing an invalid project" do + context "when passing an invalid project" do let(:project) { build(:project) } it_behaves_like "resource not available" end - context "context when passing an invalid issue" do + context "when passing an invalid issue" do let(:issue) { build(:issue) } it_behaves_like "resource not available" diff --git a/spec/graphql/mutations/issues/set_assignees_spec.rb b/spec/graphql/mutations/issues/set_assignees_spec.rb index 9a27c5acdac..4cc49e76bc6 100644 --- a/spec/graphql/mutations/issues/set_assignees_spec.rb +++ b/spec/graphql/mutations/issues/set_assignees_spec.rb @@ -11,7 +11,12 @@ RSpec.describe Mutations::Issues::SetAssignees do subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } describe '#resolve' do - subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, assignee_usernames: [assignee.username]) } + subject do + mutation.resolve(project_path: issue.project.full_path, + iid: issue.iid, + operation_mode: Types::MutationOperationModeEnum.default_mode, + assignee_usernames: [assignee.username]) + end it_behaves_like 'permission level for issue mutation is correctly verified' end diff --git a/spec/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb index e2eab591341..9b0460bc709 100644 --- a/spec/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -11,7 +11,12 @@ RSpec.describe Mutations::MergeRequests::SetAssignees do subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } describe '#resolve' do - subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: [assignee.username]) } + subject do + mutation.resolve(project_path: merge_request.project.full_path, + iid: merge_request.iid, + operation_mode: described_class.arguments['operationMode'].default_value, + assignee_usernames: [assignee.username]) + end it_behaves_like 'permission level for merge request mutation is correctly verified' end diff --git a/spec/graphql/mutations/release_asset_links/delete_spec.rb b/spec/graphql/mutations/release_asset_links/delete_spec.rb new file mode 100644 index 00000000000..15d320b58ee --- /dev/null +++ b/spec/graphql/mutations/release_asset_links/delete_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::ReleaseAssetLinks::Delete do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be_with_reload(:release) { create(:release, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } + let_it_be_with_reload(:release_link) { create(:release_link, release: release) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + let(:mutation_arguments) { { id: release_link.to_global_id } } + + describe '#resolve' do + subject(:resolve) do + mutation.resolve(**mutation_arguments) + end + + let(:deleted_link) { subject[:link] } + + context 'when the current user has access to delete the link' do + let(:current_user) { maintainer } + + it 'deletes the link and returns it', :aggregate_failures do + expect(deleted_link).to eq(release_link) + + expect(release.links).to be_empty + end + + context "when the link doesn't exist" do + let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") } + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context "when the provided ID is invalid" do + let(:mutation_arguments) { super().merge(id: 'not-a-valid-gid') } + + it 'raises an error' do + expect { subject }.to raise_error(::GraphQL::CoercionError) + end + end + end + + context 'when the current user does not have access to delete the link' do + let(:current_user) { developer } + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end +end diff --git a/spec/graphql/mutations/release_asset_links/update_spec.rb b/spec/graphql/mutations/release_asset_links/update_spec.rb index 065089066f1..20c1c8b581c 100644 --- a/spec/graphql/mutations/release_asset_links/update_spec.rb +++ b/spec/graphql/mutations/release_asset_links/update_spec.rb @@ -166,7 +166,7 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do end context "when the link doesn't exist" do - let(:mutation_arguments) { super().merge(id: 'gid://gitlab/Releases::Link/999999') } + let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") } it 'raises an error' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) diff --git a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb index 2cd61dd7bcf..a4d1101bc4f 100644 --- a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb +++ b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb @@ -14,7 +14,9 @@ RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } let_it_be(:other_proj_integration) { create(:alert_management_http_integration) } - subject { sync(resolve_http_integrations) } + let(:params) { {} } + + subject { sync(resolve_http_integrations(params)) } before do project.add_developer(developer) @@ -41,11 +43,25 @@ RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do let(:current_user) { maintainer } it { is_expected.to contain_exactly(active_http_integration) } + + context 'when HTTP Integration ID is given' do + context 'when integration is from the current project' do + let(:params) { { id: global_id_of(inactive_http_integration) } } + + it { is_expected.to contain_exactly(inactive_http_integration) } + end + + context 'when integration is from other project' do + let(:params) { { id: global_id_of(other_proj_integration) } } + + it { is_expected.to be_empty } + end + end end private def resolve_http_integrations(args = {}, context = { current_user: current_user }) - resolve(described_class, obj: project, ctx: context) + resolve(described_class, obj: project, args: args, ctx: context) end end diff --git a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb index 36e409e0677..fb0fb6729d4 100644 --- a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb +++ b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb @@ -7,12 +7,16 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } + let_it_be(:project2) { create(:project) } let_it_be(:prometheus_integration) { create(:prometheus_service, project: project) } let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) } let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } - let_it_be(:other_proj_integration) { create(:alert_management_http_integration) } + let_it_be(:other_proj_integration) { create(:alert_management_http_integration, project: project2) } + let_it_be(:other_proj_prometheus_integration) { create(:prometheus_service, project: project2) } - subject { sync(resolve_http_integrations) } + let(:params) { {} } + + subject { sync(resolve_http_integrations(params)) } specify do expect(described_class).to have_nullable_graphql_type(Types::AlertManagement::IntegrationType.connection_type) @@ -25,14 +29,43 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do context 'user has permission' do before do project.add_maintainer(current_user) + project2.add_maintainer(current_user) end it { is_expected.to contain_exactly(active_http_integration, prometheus_integration) } + + context 'when HTTP Integration ID is given' do + context 'when integration is from the current project' do + let(:params) { { id: global_id_of(inactive_http_integration) } } + + it { is_expected.to contain_exactly(inactive_http_integration) } + end + + context 'when integration is from other project' do + let(:params) { { id: global_id_of(other_proj_integration) } } + + it { is_expected.to be_empty } + end + end + + context 'when Prometheus Integration ID is given' do + context 'when integration is from the current project' do + let(:params) { { id: global_id_of(prometheus_integration) } } + + it { is_expected.to contain_exactly(prometheus_integration) } + end + + context 'when integration is from other project' do + let(:params) { { id: global_id_of(other_proj_prometheus_integration) } } + + it { is_expected.to be_empty } + end + end end private def resolve_http_integrations(args = {}, context = { current_user: current_user }) - resolve(described_class, obj: project, ctx: context) + resolve(described_class, obj: project, args: args, ctx: context) end end diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb new file mode 100644 index 00000000000..bc0344796ee --- /dev/null +++ b/spec/graphql/resolvers/blobs_resolver_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::BlobsResolver do + include GraphqlHelpers + + describe '.resolver_complexity' do + it 'adds one per path being resolved' do + control = described_class.resolver_complexity({}, child_complexity: 1) + + expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1)) + .to eq(control + 3) + end + end + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + + let(:repository) { project.repository } + let(:args) { { paths: paths, ref: ref } } + let(:paths) { [] } + let(:ref) { nil } + + subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) } + + context 'when unauthorized' do + it 'raises an exception' do + expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when authorized' do + before do + project.add_developer(user) + end + + context 'using no filter' do + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'using paths filter' do + let(:paths) { ['README.md'] } + + it 'returns the specified blobs for HEAD' do + is_expected.to contain_exactly(have_attributes(path: 'README.md')) + end + + context 'specifying a non-existent blob' do + let(:paths) { ['non-existent'] } + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'specifying a different ref' do + let(:ref) { 'add-pdf-file' } + let(:paths) { ['files/pdf/test.pdf', 'README.md'] } + + it 'returns the specified blobs for that ref' do + is_expected.to contain_exactly( + have_attributes(path: 'files/pdf/test.pdf'), + have_attributes(path: 'README.md') + ) + end + end + end + end + end +end diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb index 5eda840854a..6ffc8b045e9 100644 --- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb +++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb @@ -39,6 +39,24 @@ RSpec.describe Resolvers::BoardListIssuesResolver do expect(result).to match_array([issue1]) end + + it 'raises an exception if both assignee_username and assignee_wildcard_id are present' do + expect do + resolve_board_list_issues(args: { filters: { assignee_username: ['username'], assignee_wildcard_id: 'NONE' } }) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + + it 'accepts assignee wildcard id NONE' do + result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'NONE' } }) + + expect(result).to match_array([issue1, issue2, issue3]) + end + + it 'accepts assignee wildcard id ANY' do + result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'ANY' } }) + + expect(result).to match_array([]) + end end end diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb index c44f6b623d7..1b69bf7f63a 100644 --- a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb @@ -13,6 +13,7 @@ RSpec.describe Resolvers::Ci::JobsResolver do create(:ci_build, :sast, name: 'DAST job', pipeline: pipeline) create(:ci_build, :dast, name: 'SAST job', pipeline: pipeline) create(:ci_build, :container_scanning, name: 'Container scanning job', pipeline: pipeline) + create(:ci_build, name: 'Job with tags', pipeline: pipeline, tag_list: ['review']) end describe '#resolve' do @@ -24,7 +25,8 @@ RSpec.describe Resolvers::Ci::JobsResolver do have_attributes(name: 'Normal job'), have_attributes(name: 'DAST job'), have_attributes(name: 'SAST job'), - have_attributes(name: 'Container scanning job') + have_attributes(name: 'Container scanning job'), + have_attributes(name: 'Job with tags') ) end end @@ -43,5 +45,18 @@ RSpec.describe Resolvers::Ci::JobsResolver do ) end end + + context 'when a job has tags' do + it "returns jobs with tags when applicable" do + jobs = resolve(described_class, obj: pipeline) + expect(jobs).to contain_exactly( + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: ['review']) + ) + end + end end end diff --git a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb index 1eb6f363d5b..3cb6e94e81e 100644 --- a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Resolvers::Ci::RunnerPlatformsResolver do subject(:resolve_subject) { resolve(described_class) } it 'returns all possible runner platforms' do - expect(resolve_subject).to include( + expect(resolve_subject).to contain_exactly( hash_including(name: :linux), hash_including(name: :osx), hash_including(name: :windows), hash_including(name: :docker), hash_including(name: :kubernetes) diff --git a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb index 3d004290d9b..13ef89023d9 100644 --- a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb @@ -8,12 +8,11 @@ RSpec.describe Resolvers::Ci::RunnerSetupResolver do describe '#resolve' do let(:user) { create(:user) } - subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: platform, architecture: 'amd64' }.merge(target_param)) } + subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: platform, architecture: 'amd64' }) } context 'with container platforms' do let(:platform) { 'docker' } let(:project) { create(:project) } - let(:target_param) { { project_id: project.to_global_id } } it 'returns install instructions' do expect(resolve_subject[:install_instructions]).not_to eq(nil) @@ -27,77 +26,9 @@ RSpec.describe Resolvers::Ci::RunnerSetupResolver do context 'with regular platforms' do let(:platform) { 'linux' } - context 'without target parameter' do - let(:target_param) { {} } - - context 'when user is not admin' do - it 'returns access error' do - expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end - - context 'when user is admin' do - before do - user.update!(admin: true) - end - - it 'returns install and register instructions' do - expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) - expect(resolve_subject.values).not_to include(nil) - end - end - end - - context 'with project target parameter' do - let(:project) { create(:project) } - let(:target_param) { { project_id: project.to_global_id } } - - context 'when user has access to admin builds on project' do - before do - project.add_maintainer(user) - end - - it 'returns install and register instructions' do - expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) - expect(resolve_subject.values).not_to include(nil) - end - end - - context 'when user does not have access to admin builds on project' do - before do - project.add_developer(user) - end - - it 'returns access error' do - expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end - end - - context 'with group target parameter' do - let(:group) { create(:group) } - let(:target_param) { { group_id: group.to_global_id } } - - context 'when user has access to admin builds on group' do - before do - group.add_owner(user) - end - - it 'returns install and register instructions' do - expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) - expect(resolve_subject.values).not_to include(nil) - end - end - - context 'when user does not have access to admin builds on group' do - before do - group.add_developer(user) - end - - it 'returns access error' do - expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) end end end diff --git a/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb b/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb new file mode 100644 index 00000000000..e78bd06b567 --- /dev/null +++ b/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::TestReportSummaryResolver do + include GraphqlHelpers + + describe '#resolve' do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + subject(:resolve_subject) { resolve(described_class, obj: pipeline) } + + context 'when pipeline has build report results' do + let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) } + + it 'returns test report summary data' do + expect(resolve_subject.keys).to contain_exactly(:total, :test_suites) + expect(resolve_subject[:test_suites][0].keys).to contain_exactly(:build_ids, :name, :total_time, :total_count, :success_count, :failed_count, :skipped_count, :error_count, :suite_error) + expect(resolve_subject[:total][:time]).to eq(0.42) + expect(resolve_subject[:total][:count]).to eq(2) + expect(resolve_subject[:total][:success]).to eq(0) + expect(resolve_subject[:total][:failed]).to eq(0) + expect(resolve_subject[:total][:skipped]).to eq(0) + expect(resolve_subject[:total][:error]).to eq(2) + expect(resolve_subject[:total][:suite_error]).to eq(nil) + end + end + + context 'when pipeline does not have build report results' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'renders test report summary data' do + expect(resolve_subject.keys).to contain_exactly(:total, :test_suites) + expect(resolve_subject[:test_suites]).to eq([]) + expect(resolve_subject[:total][:time]).to eq(0) + expect(resolve_subject[:total][:count]).to eq(0) + expect(resolve_subject[:total][:success]).to eq(0) + expect(resolve_subject[:total][:failed]).to eq(0) + expect(resolve_subject[:total][:skipped]).to eq(0) + expect(resolve_subject[:total][:error]).to eq(0) + expect(resolve_subject[:total][:suite_error]).to eq(nil) + end + end + end +end diff --git a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb new file mode 100644 index 00000000000..606c6eb03a3 --- /dev/null +++ b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::TestSuiteResolver do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + + describe '#resolve' do + subject(:test_suite) { resolve(described_class, obj: pipeline, args: { build_ids: build_ids }) } + + context 'when pipeline has builds with test reports' do + let_it_be(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') } + + let(:suite_name) { 'test' } + let(:build_ids) { pipeline.latest_builds.pluck(:id) } + + before do + build = main_pipeline.builds.last + build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window + + # The JUnit fixture for the given build has 3 failures. + # This service will create 1 test case failure record for each. + Ci::TestFailureHistoryService.new(main_pipeline).execute + end + + it 'renders test suite data' do + expect(test_suite[:name]).to eq('test') + + # Each test failure in this pipeline has a matching failure in the default branch + recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] } + expect(recent_failures).to eq([ + { count: 1, base_branch: 'master' }, + { count: 1, base_branch: 'master' }, + { count: 1, base_branch: 'master' } + ]) + end + end + + context 'when pipeline has no builds that matches the given build_ids' do + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + + let(:suite_name) { 'test' } + let(:build_ids) { [non_existing_record_id] } + + it 'returns nil' do + expect(test_suite).to be_nil + end + end + end +end diff --git a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb index 27ac1572cab..4c244da5c62 100644 --- a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb +++ b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb @@ -38,11 +38,8 @@ RSpec.describe LooksAhead do user = Class.new(GraphQL::Schema::Object) do graphql_name 'User' field :name, String, null: true - field :issues, issue.connection_type, - null: true - field :issues_with_lookahead, issue.connection_type, - resolver: issues_resolver, - null: true + field :issues, issue.connection_type, null: true + field :issues_with_lookahead, issue.connection_type, resolver: issues_resolver, null: true end Class.new(GraphQL::Schema) do @@ -101,7 +98,7 @@ RSpec.describe LooksAhead do expect(res['errors']).to be_blank expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name) - %w(issues issuesWithLookahead).each do |field| + %w[issues issuesWithLookahead].each do |field| expect(all_issue_titles(res, field)).to match_array(issue_titles) expect(all_label_ids(res, field)).to match_array(expected_label_ids) end diff --git a/spec/graphql/resolvers/group_milestones_resolver_spec.rb b/spec/graphql/resolvers/group_milestones_resolver_spec.rb index d8ff8e9c1f2..dd3f1676538 100644 --- a/spec/graphql/resolvers/group_milestones_resolver_spec.rb +++ b/spec/graphql/resolvers/group_milestones_resolver_spec.rb @@ -136,5 +136,56 @@ RSpec.describe Resolvers::GroupMilestonesResolver do expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3]) end end + + describe 'include_descendants and include_ancestors' do + let_it_be(:parent_group) { create(:group, :public) } + let_it_be(:group) { create(:group, :public, parent: parent_group) } + let_it_be(:accessible_group) { create(:group, :private, parent: group) } + let_it_be(:accessible_project) { create(:project, group: accessible_group) } + let_it_be(:inaccessible_group) { create(:group, :private, parent: group) } + let_it_be(:inaccessible_project) { create(:project, :private, group: group) } + let_it_be(:milestone1) { create(:milestone, group: group) } + let_it_be(:milestone2) { create(:milestone, group: accessible_group) } + let_it_be(:milestone3) { create(:milestone, project: accessible_project) } + let_it_be(:milestone4) { create(:milestone, group: inaccessible_group) } + let_it_be(:milestone5) { create(:milestone, project: inaccessible_project) } + let_it_be(:milestone6) { create(:milestone, group: parent_group) } + + before do + accessible_group.add_developer(current_user) + end + + context 'when including neither ancestor or descendant milestones in a public group' do + let(:args) { {} } + + it 'finds milestones only in accessible projects and groups' do + expect(resolve_group_milestones(args)).to match_array([milestone1]) + end + end + + context 'when including descendant milestones in a public group' do + let(:args) { { include_descendants: true } } + + it 'finds milestones only in accessible projects and groups' do + expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3]) + end + end + + context 'when including ancestor milestones in a public group' do + let(:args) { { include_ancestors: true } } + + it 'finds milestones only in accessible projects and groups' do + expect(resolve_group_milestones(args)).to match_array([milestone1, milestone6]) + end + end + + context 'when including both ancestor or descendant milestones in a public group' do + let(:args) { { include_descendants: true, include_ancestors: true } } + + it 'finds milestones only in accessible projects and groups' do + expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3, milestone6]) + end + end + end end end diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb index decc3569d6c..3fbd9bd2368 100644 --- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb +++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb @@ -69,6 +69,14 @@ RSpec.describe Resolvers::IssueStatusCountsResolver do expect(result.closed).to eq 1 end + context 'when both assignee_username and assignee_usernames are provided' do + it 'raises a mutually exclusive filter error' do + expect do + resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') + end + end + private def resolve_issue_status_counts(args = {}, context = { current_user: current_user }) diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 6e802bf7d25..7c2ceb50066 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -46,10 +46,6 @@ RSpec.describe Resolvers::IssuesResolver do expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1) end - it 'filters by assignee_username' do - expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) - end - it 'filters by two assignees' do assignee2 = create(:user) issue2.update!(assignees: [assignee, assignee2]) @@ -78,6 +74,24 @@ RSpec.describe Resolvers::IssuesResolver do expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) end + describe 'filters by assignee_username' do + it 'filters by assignee_username' do + expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) + end + + it 'filters by assignee_usernames' do + expect(resolve_issues(assignee_usernames: [assignee.username])).to contain_exactly(issue2) + end + + context 'when both assignee_username and assignee_usernames are provided' do + it 'raises a mutually exclusive filter error' do + expect do + resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') + end + end + end + describe 'filters by created_at' do it 'filters by created_before' do expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1) @@ -144,6 +158,29 @@ RSpec.describe Resolvers::IssuesResolver do end end + describe 'filters by negated params' do + it 'returns issues without the specified iids' do + expect(resolve_issues(not: { iids: [issue1.iid] })).to contain_exactly(issue2) + end + + it 'returns issues without the specified label names' do + expect(resolve_issues(not: { label_name: [label1.title] })).to be_empty + expect(resolve_issues(not: { label_name: [label2.title] })).to contain_exactly(issue1) + end + + it 'returns issues without the specified milestone' do + expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2) + end + + it 'returns issues without the specified assignee_usernames' do + expect(resolve_issues(not: { assignee_usernames: [assignee.username] })).to contain_exactly(issue1) + end + + it 'returns issues without the specified assignee_id' do + expect(resolve_issues(not: { assignee_id: [assignee.id] })).to contain_exactly(issue1) + end + end + describe 'sorting' do context 'when sorting by created' do it 'sorts issues ascending' do diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 7dd968d90a8..aec6c6c6708 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do include SortingHelper let_it_be(:project) { create(:project, :repository) } + let_it_be(:other_project) { create(:project, :repository) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:current_user) { create(:user) } let_it_be(:other_user) { create(:user) } @@ -16,10 +17,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) } let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) } let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) } - let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2, project: project), **common_attrs) } - let_it_be(:merge_request_with_milestone) { create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) } - let_it_be(:other_project) { create(:project, :repository) } - let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } + let_it_be(:merge_request_6) do + create(:labeled_merge_request, :unique_branches, **common_attrs, labels: create_list(:label, 2, project: project)) + end + + let_it_be(:merge_request_with_milestone) do + create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) + end + + let_it_be(:other_merge_request) do + create(:merge_request, source_project: other_project, target_project: other_project) + end let(:iid_1) { merge_request_1.iid } let(:iid_2) { merge_request_2.iid } @@ -41,13 +49,16 @@ RSpec.describe Resolvers::MergeRequestsResolver do # AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC # SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2 # SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2 - let(:queries_per_project) { 3 } + let(:queries_per_project) { 4 } - context 'no arguments' do + context 'without arguments' do it 'returns all merge requests' do result = resolve_mr(project) - expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone) + expect(result).to contain_exactly( + merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, + merge_request_6, merge_request_with_milestone + ) end it 'returns only merge requests that the current user can see' do @@ -57,7 +68,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by iid alone' do + context 'with iid alone' do it 'batch-resolves by target project full path and individual IID', :request_store do # 1 query for project_authorizations, and 1 for merge_requests result = batch_sync(max_queries: queries_per_project) do @@ -83,7 +94,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3) end - it 'can batch-resolve merge requests from different projects', :request_store, :use_clean_rails_memory_store_caching do + it 'can batch-resolve merge requests from different projects', :request_store do # 2 queries for project_authorizations, and 2 for merge_requests results = batch_sync(max_queries: queries_per_project * 2) do a = resolve_mr(project, iids: [iid_1]) @@ -121,7 +132,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by source branches' do + context 'with source branches argument' do it 'takes one argument' do result = resolve_mr(project, source_branches: [merge_request_3.source_branch]) @@ -131,13 +142,13 @@ RSpec.describe Resolvers::MergeRequestsResolver do it 'takes more than one argument' do mrs = [merge_request_3, merge_request_4] branches = mrs.map(&:source_branch) - result = resolve_mr(project, source_branches: branches ) + result = resolve_mr(project, source_branches: branches) expect(result).to match_array(mrs) end end - context 'by target branches' do + context 'with target branches argument' do it 'takes one argument' do result = resolve_mr(project, target_branches: [merge_request_3.target_branch]) @@ -153,7 +164,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by state' do + context 'with state argument' do it 'takes one argument' do result = resolve_mr(project, state: 'locked') @@ -161,7 +172,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by label' do + context 'with label argument' do let_it_be(:label) { merge_request_6.labels.first } let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } @@ -178,7 +189,18 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by merged_after and merged_before' do + context 'with negated label argument' do + let_it_be(:label) { merge_request_6.labels.first } + let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } + + it 'excludes merge requests with given label from selection' do + result = resolve_mr(project, not: { labels: [label.title] }) + + expect(result).not_to include(merge_request_6, with_label) + end + end + + context 'with merged_after and merged_before arguments' do before do merge_request_1.metrics.update!(merged_at: 10.days.ago) end @@ -196,7 +218,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end - context 'by milestone' do + context 'with milestone argument' do it 'filters merge requests by milestone title' do result = resolve_mr(project, milestone_title: milestone.title) @@ -210,9 +232,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end + context 'with negated milestone argument' do + it 'filters out merge requests with given milestone title' do + result = resolve_mr(project, not: { milestone_title: milestone.title }) + + expect(result).not_to include(merge_request_with_milestone) + end + end + describe 'combinations' do it 'requires all filters' do - create(:merge_request, :closed, source_project: project, target_project: project, source_branch: merge_request_4.source_branch) + create(:merge_request, :closed, **common_attrs, source_branch: merge_request_4.source_branch) result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked') diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb index 147a02e1d79..618d012bd6d 100644 --- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb @@ -112,7 +112,7 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do subject(:projects) { resolve_projects(args) } let(:include_subgroups) { false } - let(:project_3) { create(:project, name: 'Project', path: 'project', namespace: namespace) } + let!(:project_3) { create(:project, name: 'Project', path: 'project', namespace: namespace) } context 'when ids is provided' do let(:ids) { [project_3.to_global_id.to_s] } diff --git a/spec/graphql/resolvers/project_jobs_resolver_spec.rb b/spec/graphql/resolvers/project_jobs_resolver_spec.rb new file mode 100644 index 00000000000..94df2999163 --- /dev/null +++ b/spec/graphql/resolvers/project_jobs_resolver_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::ProjectJobsResolver do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:irrelevant_project) { create(:project, :repository) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:irrelevant_pipeline) { create(:ci_pipeline, project: irrelevant_project) } + let_it_be(:build_one) { create(:ci_build, :success, name: 'Build One', pipeline: pipeline) } + let_it_be(:build_two) { create(:ci_build, :success, name: 'Build Two', pipeline: pipeline) } + let_it_be(:build_three) { create(:ci_build, :failed, name: 'Build Three', pipeline: pipeline) } + + let(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline)} + let(:args) { {} } + let(:current_user) { create(:user) } + + subject { resolve_jobs(args) } + + describe '#resolve' do + context 'with authorized user' do + before do + project.add_developer(current_user) + end + + context 'with statuses argument' do + let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } } + + it { is_expected.to contain_exactly(build_one, build_two) } + end + + context 'without statuses argument' do + it { is_expected.to contain_exactly(build_one, build_two, build_three) } + end + end + + context 'with unauthorized user' do + let(:current_user) { nil } + + it { is_expected.to be_nil } + end + end + + private + + def resolve_jobs(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, args: args, ctx: context) + end +end diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb index 69127c4b061..3d33e0b500d 100644 --- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') } + let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') } let_it_be(:other_pipeline) { create(:ci_pipeline) } let(:current_user) { create(:user) } @@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'resolves pipeline for the passed iid' do + expect(Ci::PipelinesFinder) + .to receive(:new) + .with(project, current_user, iids: ['1234']) + .and_call_original + result = batch_sync do resolve_pipeline(project, { iid: '1234' }) end @@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'resolves pipeline for the passed sha' do + expect(Ci::PipelinesFinder) + .to receive(:new) + .with(project, current_user, sha: ['sha']) + .and_call_original + result = batch_sync do resolve_pipeline(project, { sha: 'sha' }) end @@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'keeps the queries under the threshold for iid' do - create(:ci_pipeline, project: project, iid: '1235') - control = ActiveRecord::QueryRecorder.new do batch_sync { resolve_pipeline(project, { iid: '1234' }) } end @@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do end it 'keeps the queries under the threshold for sha' do - create(:ci_pipeline, project: project, sha: 'sha2') - control = ActiveRecord::QueryRecorder.new do batch_sync { resolve_pipeline(project, { sha: 'sha' }) } end diff --git a/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb new file mode 100644 index 00000000000..398dd7a2e2e --- /dev/null +++ b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::RepositoryBranchNamesResolver do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + + describe '#resolve' do + subject(:resolve_branch_names) do + resolve( + described_class, + obj: project.repository, + args: { search_pattern: pattern }, + ctx: { current_user: project.creator } + ) + end + + context 'with empty search pattern' do + let(:pattern) { '' } + + it 'returns nil' do + expect(resolve_branch_names).to eq(nil) + end + end + + context 'with a valid search pattern' do + let(:pattern) { 'mas*' } + + it 'returns matching branches' do + expect(resolve_branch_names).to match_array(['master']) + end + end + end +end diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb new file mode 100644 index 00000000000..585cd657e35 --- /dev/null +++ b/spec/graphql/resolvers/timelog_resolver_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::TimelogResolver do + include GraphqlHelpers + + 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, :public, group: group) } + + before_all do + group.add_developer(current_user) + project.add_developer(current_user) + end + + before do + group.clear_memoization(:timelogs) + end + + describe '#resolve' do + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, 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: issue2, spent_at: 2.days.ago.end_of_day) } + let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) } + + let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } } + + it 'finds all timelogs within given dates' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1) + end + + it 'return nothing when user has insufficient permissions' do + user = create(:user) + group.add_guest(current_user) + + expect(resolve_timelogs(user: user, **args)).to be_empty + 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 arguments are invalid' do + let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError } + + context 'when no time or date arguments are present' do + let(:args) { {} } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Start and End arguments must be present/) + end + end + + context 'when only start_time is present' do + let(:args) { { start_time: 6.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Both Start and End arguments must be present/) + end + end + + context 'when only end_time is present' do + let(:args) { { end_time: 2.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Both Start and End arguments must be present/) + end + end + + context 'when only start_date is present' do + let(:args) { { start_date: 6.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Both Start and End arguments must be present/) + end + end + + context 'when only end_date is present' do + let(:args) { { end_date: 2.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Both Start and End arguments must be present/) + end + end + + 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, /Both Start and End arguments must be present/) + end + end + + 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, /Both Start and End arguments must be present/) + end + end + + context 'when three arguments are present' do + let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Only Time or Date arguments must be present/) + end + end + + context 'when start argument is after end argument' do + let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /Start argument must be before End argument/) + end + end + + context 'when time range is more than 60 days' do + let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } } + + it 'returns correct error' do + expect { resolve_timelogs(**args) } + .to raise_error(error_class, /The time range period cannot contain more than 60 days/) + end + end + end + end + end + + def resolve_timelogs(user: current_user, **args) + context = { current_user: user } + resolve(described_class, obj: group, args: args, ctx: context) + end +end diff --git a/spec/graphql/resolvers/users/snippets_resolver_spec.rb b/spec/graphql/resolvers/users/snippets_resolver_spec.rb index 11a5b7517e0..04fe3213a99 100644 --- a/spec/graphql/resolvers/users/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/users/snippets_resolver_spec.rb @@ -75,9 +75,19 @@ RSpec.describe Resolvers::Users::SnippetsResolver do end.to raise_error(GraphQL::CoercionError) end end + + context 'when user profile is private' do + it 'does not return snippets for that user' do + expect(resolve_snippets(obj: other_user)).to contain_exactly(other_personal_snippet, other_project_snippet) + + other_user.update!(private_profile: true) + + expect(resolve_snippets(obj: other_user)).to be_empty + end + end end - def resolve_snippets(args: {}) - resolve(described_class, args: args, ctx: { current_user: current_user }, obj: current_user) + def resolve_snippets(args: {}, context_user: current_user, obj: current_user) + resolve(described_class, args: args, ctx: { current_user: context_user }, obj: obj) end end diff --git a/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb b/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb index c50092d7f0e..d1c2b4044c1 100644 --- a/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb +++ b/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb @@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['UsageTrendsMeasurement'] do describe 'authorization' do let_it_be(:measurement) { create(:usage_trends_measurement, :project_count) } + let(:user) { create(:user) } let(:query) do @@ -44,7 +45,7 @@ RSpec.describe GitlabSchema.types['UsageTrendsMeasurement'] do let(:user) { create(:user, :admin) } before do - stub_feature_flags(user_mode_in_session: false) + stub_application_setting(admin_mode: false) end it 'returns data' do diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb index b10c2a2ab2a..d057afb331c 100644 --- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb +++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb @@ -48,15 +48,21 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do end end - context 'without project' do - let_it_be(:integration) { create(:prometheus_service, project: nil, group: create(:group)) } - - it_behaves_like 'has field with value', 'token' do - let(:value) { nil } - end - - it_behaves_like 'has field with value', 'url' do - let(:value) { nil } + describe 'a group integration' do + let_it_be(:group) { create(:group) } + let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) } + + # Since it is impossible to authorize the parent here, given that the + # project is nil, all fields should be redacted: + + described_class.fields.each_key do |field_name| + context "field: #{field_name}" do + it 'is redacted' do + expect do + resolve_field(field_name, integration, current_user: user) + end.to raise_error(GraphqlHelpers::UnauthorizedObject) + end + end end end end diff --git a/spec/graphql/types/base_enum_spec.rb b/spec/graphql/types/base_enum_spec.rb index 744aee40044..bab0278ee25 100644 --- a/spec/graphql/types/base_enum_spec.rb +++ b/spec/graphql/types/base_enum_spec.rb @@ -3,6 +3,38 @@ require 'spec_helper' RSpec.describe Types::BaseEnum do + describe '.from_rails_enum' do + let(:enum_type) { Class.new(described_class) } + let(:template) { "The name is '%{name}', James %{name}." } + + let(:enum) do + { + 'foo' => 1, + 'bar' => 2, + 'baz' => 100 + } + end + + it 'contructs the correct values' do + enum_type.from_rails_enum(enum, description: template) + + expect(enum_type.values).to match( + 'FOO' => have_attributes( + description: "The name is 'foo', James foo.", + value: 'foo' + ), + 'BAR' => have_attributes( + description: "The name is 'bar', James bar.", + value: 'bar' + ), + 'BAZ' => have_attributes( + description: "The name is 'baz', James baz.", + value: 'baz' + ) + ) + end + end + describe '.declarative_enum' do let(:use_name) { true } let(:use_description) { true } @@ -26,12 +58,15 @@ RSpec.describe Types::BaseEnum do end end - subject(:set_declarative_enum) { enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description) } + subject(:set_declarative_enum) do + enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description) + end describe '#graphql_name' do context 'when the use_name is `true`' do it 'changes the graphql_name' do - expect { set_declarative_enum }.to change { enum_type.graphql_name }.from('OriginalName').to('Name') + expect { set_declarative_enum } + .to change(enum_type, :graphql_name).from('OriginalName').to('Name') end end @@ -39,7 +74,8 @@ RSpec.describe Types::BaseEnum do let(:use_name) { false } it 'does not change the graphql_name' do - expect { set_declarative_enum }.not_to change { enum_type.graphql_name }.from('OriginalName') + expect { set_declarative_enum } + .not_to change(enum_type, :graphql_name).from('OriginalName') end end end @@ -47,7 +83,8 @@ RSpec.describe Types::BaseEnum do describe '#description' do context 'when the use_description is `true`' do it 'changes the description' do - expect { set_declarative_enum }.to change { enum_type.description }.from('Original description').to('Description') + expect { set_declarative_enum } + .to change(enum_type, :description).from('Original description').to('Description') end end @@ -55,7 +92,8 @@ RSpec.describe Types::BaseEnum do let(:use_description) { false } it 'does not change the description' do - expect { set_declarative_enum }.not_to change { enum_type.description }.from('Original description') + expect { set_declarative_enum } + .not_to change(enum_type, :description).from('Original description') end end end diff --git a/spec/graphql/types/base_object_spec.rb b/spec/graphql/types/base_object_spec.rb new file mode 100644 index 00000000000..d8f2ef58ea5 --- /dev/null +++ b/spec/graphql/types/base_object_spec.rb @@ -0,0 +1,432 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::BaseObject do + include GraphqlHelpers + + describe 'scoping items' do + let_it_be(:custom_auth) do + Class.new(::Gitlab::Graphql::Authorize::ObjectAuthorization) do + def any? + true + end + + def ok?(object, _current_user) + return false if object == { id: 100 } + return false if object.try(:deactivated?) + + true + end + end + end + + let_it_be(:test_schema) do + auth = custom_auth.new(nil) + + base_object = Class.new(described_class) do + # Override authorization so we don't need to mock Ability + define_singleton_method :authorization do + auth + end + end + + y_type = Class.new(base_object) do + graphql_name 'Y' + authorize :read_y + field :id, Integer, null: false + + def id + object[:id] + end + end + + number_type = Module.new do + include ::Types::BaseInterface + + graphql_name 'Number' + + field :value, Integer, null: false + end + + odd_type = Class.new(described_class) do + graphql_name 'Odd' + implements number_type + + authorize :read_odd + field :odd_value, Integer, null: false + + def odd_value + object[:value] + end + end + + even_type = Class.new(described_class) do + graphql_name 'Even' + implements number_type + + authorize :read_even + field :even_value, Integer, null: false + + def even_value + object[:value] + end + end + + # an abstract type, delegating authorization to members + odd_or_even = Class.new(::Types::BaseUnion) do + graphql_name 'OddOrEven' + + possible_types odd_type, even_type + + define_singleton_method :resolve_type do |object, ctx| + if object[:value].odd? + odd_type + else + even_type + end + end + end + + number_type.define_singleton_method :resolve_type do |object, ctx| + odd_or_even.resolve_type(object, ctx) + end + + x_type = Class.new(base_object) do + graphql_name 'X' + # Scalar types + field :title, String, null: true + # monomorphic types + field :lazy_list_of_ys, [y_type], null: true + field :list_of_lazy_ys, [y_type], null: true + field :array_ys_conn, y_type.connection_type, null: true + # polymorphic types + field :polymorphic_conn, odd_or_even.connection_type, null: true + field :polymorphic_object, odd_or_even, null: true do + argument :value, Integer, required: true + end + field :interface_conn, number_type.connection_type, null: true + + def lazy_list_of_ys + ::Gitlab::Graphql::Lazy.new { object[:ys] } + end + + def list_of_lazy_ys + object[:ys].map { |y| ::Gitlab::Graphql::Lazy.new { y } } + end + + def array_ys_conn + object[:ys].dup + end + + def polymorphic_conn + object[:values].dup + end + alias_method :interface_conn, :polymorphic_conn + + def polymorphic_object(value) + value + end + end + + user_type = Class.new(base_object) do + graphql_name 'User' + authorize :read_user + field 'name', String, null: true + end + + Class.new(GraphQL::Schema) do + lazy_resolve ::Gitlab::Graphql::Lazy, :force + use ::GraphQL::Pagination::Connections + use ::Gitlab::Graphql::Pagination::Connections + + query(Class.new(::Types::BaseObject) do + graphql_name 'Query' + field :x, x_type, null: true + field :users, user_type.connection_type, null: true + + def x + ::Gitlab::Graphql::Lazy.new { context[:x] } + end + + def users + ::Gitlab::Graphql::Lazy.new { User.id_in(context[:user_ids]).order(id: :asc) } + end + end) + + def unauthorized_object(err) + nil + end + end + end + + def document(path) + GraphQL.parse(<<~GQL) + query { + x { + title + #{query_graphql_path(path, 'id')} + } + } + GQL + end + + let(:data) do + { + x: { + title: 'Hey', + ys: [{ id: 1 }, { id: 100 }, { id: 2 }] + } + } + end + + shared_examples 'array member redaction' do |path| + let(:result) do + query = GraphQL::Query.new(test_schema, document: document(path), context: data) + query.result.to_h + end + + it 'redacts the unauthorized array member' do + expect(graphql_dig_at(result, 'data', 'x', 'title')).to eq('Hey') + expect(graphql_dig_at(result, 'data', 'x', *path)).to contain_exactly( + eq({ 'id' => 1 }), + eq({ 'id' => 2 }) + ) + end + end + + # For example a batchloaded association + describe 'a lazy list' do + it_behaves_like 'array member redaction', %w[lazyListOfYs] + end + + # For example using a batchloader to map over a set of IDs + describe 'a list of lazy items' do + it_behaves_like 'array member redaction', %w[listOfLazyYs] + end + + describe 'an array connection of items' do + it_behaves_like 'array member redaction', %w[arrayYsConn nodes] + end + + describe 'an array connection of items, selecting edges' do + it_behaves_like 'array member redaction', %w[arrayYsConn edges node] + end + + it 'paginates arrays correctly' do + n = 7 + + data = { + x: { + ys: (95..105).to_a.map { |id| { id: id } } + } + } + + doc = lambda do |after| + GraphQL.parse(<<~GQL) + query { + x { + ys: arrayYsConn(#{attributes_to_graphql(first: n, after: after)}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + } + nodes { id } + } + } + } + GQL + end + returned_items = ->(ids) { ids.to_a.map { |id| eq({ 'id' => id }) } } + + query = GraphQL::Query.new(test_schema, document: doc[nil], context: data) + result = query.result.to_h + + ys = result.dig('data', 'x', 'ys', 'nodes') + page = result.dig('data', 'x', 'ys', 'pageInfo') + # We expect this page to be smaller, since we paginate before redaction + expect(ys).to match_array(returned_items[(95..101).to_a - [100]]) + expect(page).to include('hasNextPage' => true, 'hasPreviousPage' => false) + + cursor = page['endCursor'] + query_2 = GraphQL::Query.new(test_schema, document: doc[cursor], context: data) + result_2 = query_2.result.to_h + + ys = result_2.dig('data', 'x', 'ys', 'nodes') + page = result_2.dig('data', 'x', 'ys', 'pageInfo') + expect(ys).to match_array(returned_items[102..105]) + expect(page).to include('hasNextPage' => false, 'hasPreviousPage' => true) + end + + it 'filters connections correctly' do + active_users = create_list(:user, 3, state: :active) + inactive = create(:user, state: :deactivated) + + data = { user_ids: [inactive, *active_users].map(&:id) } + + doc = GraphQL.parse(<<~GQL) + query { + users { nodes { name } } + } + GQL + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + expect(result.dig('data', 'users', 'nodes')).to match_array(active_users.map do |u| + eq({ 'name' => u.name }) + end) + end + + it 'filters polymorphic connections' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + things: polymorphicConn { + nodes { + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_even, { value: 2 }).and_return(false) + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_even, { value: 4 }).and_return(true) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + things = result.dig('data', 'x', 'things', 'nodes') + + expect(things).to contain_exactly( + { 'oddValue' => 1 }, + { 'evenValue' => 4 } + ) + end + + it 'filters interface connections' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + things: interfaceConn { + nodes { + value + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_even, { value: 2 }).and_return(false) + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_even, { value: 4 }).and_return(true) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + things = result.dig('data', 'x', 'things', 'nodes') + + expect(things).to contain_exactly( + { 'value' => 1, 'oddValue' => 1 }, + { 'value' => 4, 'evenValue' => 4 } + ) + end + + it 'redacts polymorphic objects' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + ok: polymorphicObject(value: 1) { + ... on Odd { oddValue } + ... on Even { evenValue } + } + bad: polymorphicObject(value: 3) { + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + expect(result.dig('data', 'x', 'ok')).to eq({ 'oddValue' => 1 }) + expect(result.dig('data', 'x', 'bad')).to be_nil + end + + it 'paginates before scoping' do + # Inactive first so they sort first + n = 3 + inactive = create_list(:user, n - 1, state: :deactivated) + active_users = create_list(:user, 2, state: :active) + + data = { user_ids: [*inactive, *active_users].map(&:id) } + + doc = GraphQL.parse(<<~GQL) + query { + users(first: #{n}) { + pageInfo { hasNextPage } + nodes { name } } + } + GQL + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + # We expect the page to be loaded and then filtered - i.e. to have all + # deactivated users removed. + expect(result.dig('data', 'users', 'pageInfo', 'hasNextPage')).to be_truthy + expect(result.dig('data', 'users', 'nodes')) + .to contain_exactly({ 'name' => active_users.first.name }) + end + end +end diff --git a/spec/graphql/types/board_type_spec.rb b/spec/graphql/types/board_type_spec.rb index dca3cfd8aaf..403fbe1f290 100644 --- a/spec/graphql/types/board_type_spec.rb +++ b/spec/graphql/types/board_type_spec.rb @@ -8,8 +8,18 @@ RSpec.describe GitlabSchema.types['Board'] do specify { expect(described_class).to require_graphql_authorizations(:read_issue_board) } it 'has specific fields' do - expected_fields = %w[id name web_url web_path] + expected_fields = %w[ + id + name + hideBacklogList + hideClosedList + createdAt + updatedAt + lists + webPath + webUrl + ] - expect(described_class).to include_graphql_fields(*expected_fields) + expect(described_class).to have_graphql_fields(*expected_fields).at_least end end diff --git a/spec/graphql/types/boards/board_issue_input_type_spec.rb b/spec/graphql/types/boards/board_issue_input_type_spec.rb index 6319ff9a88e..5d3efb9b40d 100644 --- a/spec/graphql/types/boards/board_issue_input_type_spec.rb +++ b/spec/graphql/types/boards/board_issue_input_type_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['BoardIssueInput'] do it { expect(described_class.graphql_name).to eq('BoardIssueInput') } - it 'exposes negated issue arguments' do + it 'has specific fields' do allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername - releaseTag myReactionEmoji not search) + releaseTag myReactionEmoji not search assigneeWildcardId) expect(described_class.arguments.keys).to include(*allowed_args) expect(described_class.arguments['not'].type).to eq(Types::Boards::NegatedBoardIssueInputType) diff --git a/spec/graphql/types/ci/job_status_enum_spec.rb b/spec/graphql/types/ci/job_status_enum_spec.rb new file mode 100644 index 00000000000..e8a1a2e0aa8 --- /dev/null +++ b/spec/graphql/types/ci/job_status_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiJobStatus'] do + it 'exposes all job status values' do + expect(described_class.values.values).to contain_exactly( + *::Ci::HasStatus::AVAILABLE_STATUSES.map do |status| + have_attributes(value: status, graphql_name: status.upcase) + end + ) + end +end diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index 25f626cea0f..787e2174070 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -8,14 +8,32 @@ RSpec.describe Types::Ci::JobType do it 'exposes the expected fields' do expected_fields = %i[ - pipeline + active + allow_failure + artifacts + cancelable + commitPath + coverage + created_at + detailedStatus + duration + finished_at + id name needs - detailedStatus + pipeline + playable + queued_at + refName + refPath + retryable scheduledAt - artifacts - finished_at - duration + schedulingType + shortSha + stage + started_at + status + tags ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index e0e84a1b635..c7d2cbdb765 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -11,8 +11,9 @@ RSpec.describe Types::Ci::PipelineType do expected_fields = %w[ id iid sha before_sha status detailed_status config_source duration coverage created_at updated_at started_at finished_at committed_at - stages user retryable cancelable jobs source_job downstream - upstream path project active user_permissions warnings commit_path + stages user retryable cancelable jobs source_job job downstream + upstream path project active user_permissions warnings commit_path uses_needs + test_report_summary test_suite ] if Gitlab.ee? diff --git a/spec/graphql/types/ci/recent_failures_type_spec.rb b/spec/graphql/types/ci/recent_failures_type_spec.rb new file mode 100644 index 00000000000..38369da46bf --- /dev/null +++ b/spec/graphql/types/ci/recent_failures_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RecentFailuresType do + specify { expect(described_class.graphql_name).to eq('RecentFailures') } + + it 'contains attributes related to a recent failure history for a test case' do + expected_fields = %w[ + count base_branch + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb index 9a8d4fa96a3..cb8c1cb02cd 100644 --- a/spec/graphql/types/ci/stage_type_spec.rb +++ b/spec/graphql/types/ci/stage_type_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do name groups detailedStatus + jobs ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/ci/test_case_status_enum_spec.rb b/spec/graphql/types/ci/test_case_status_enum_spec.rb new file mode 100644 index 00000000000..ba2d1aefb20 --- /dev/null +++ b/spec/graphql/types/ci/test_case_status_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestCaseStatusEnum do + specify { expect(described_class.graphql_name).to eq('TestCaseStatus') } + + it 'exposes all test case status types' do + expect(described_class.values.keys).to eq( + ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES + ) + end +end diff --git a/spec/graphql/types/ci/test_case_type_spec.rb b/spec/graphql/types/ci/test_case_type_spec.rb new file mode 100644 index 00000000000..e6cd70c287e --- /dev/null +++ b/spec/graphql/types/ci/test_case_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestCaseType do + specify { expect(described_class.graphql_name).to eq('TestCase') } + + it 'contains attributes related to a pipeline test case' do + expected_fields = %w[ + name status classname file attachment_url execution_time stack_trace system_output recent_failures + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_report_summary_type_spec.rb b/spec/graphql/types/ci/test_report_summary_type_spec.rb new file mode 100644 index 00000000000..06974da0b88 --- /dev/null +++ b/spec/graphql/types/ci/test_report_summary_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestReportSummaryType do + specify { expect(described_class.graphql_name).to eq('TestReportSummary') } + + it 'contains attributes related to a pipeline test report summary' do + expected_fields = %w[ + total test_suites + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_report_total_type_spec.rb b/spec/graphql/types/ci/test_report_total_type_spec.rb new file mode 100644 index 00000000000..e5b7b358edb --- /dev/null +++ b/spec/graphql/types/ci/test_report_total_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestReportTotalType do + specify { expect(described_class.graphql_name).to eq('TestReportTotal') } + + it 'contains attributes related to a pipeline test report summary' do + expected_fields = %w[ + time count success failed skipped error suite_error + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_suite_summary_type_spec.rb b/spec/graphql/types/ci/test_suite_summary_type_spec.rb new file mode 100644 index 00000000000..e87782037c7 --- /dev/null +++ b/spec/graphql/types/ci/test_suite_summary_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestSuiteSummaryType do + specify { expect(described_class.graphql_name).to eq('TestSuiteSummary') } + + it 'contains attributes related to a pipeline test report summary' do + expected_fields = %w[ + name total_time total_count success_count failed_count skipped_count error_count suite_error build_ids + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_suite_type_spec.rb b/spec/graphql/types/ci/test_suite_type_spec.rb new file mode 100644 index 00000000000..d9caca3e2c3 --- /dev/null +++ b/spec/graphql/types/ci/test_suite_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestSuiteType do + specify { expect(described_class.graphql_name).to eq('TestSuite') } + + it 'contains attributes related to a pipeline test suite' do + expected_fields = %w[ + name total_time total_count success_count failed_count skipped_count error_count suite_error test_cases + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb index 8eb023ad2a3..4df51dc8d1b 100644 --- a/spec/graphql/types/global_id_type_spec.rb +++ b/spec/graphql/types/global_id_type_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Types::GlobalIDType do let_it_be(:project) { create(:project) } + let(:gid) { project.to_global_id } it 'is has the correct name' do diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index 21fc530149c..6908a610aae 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status design_collection alert_management_alert severity current_user_todos moved moved_to - create_note_email] + create_note_email timelogs] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb new file mode 100644 index 00000000000..486e1c4f502 --- /dev/null +++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do + it 'the correct enum members' do + expect(described_class.values).to match( + 'REVIEWED' => have_attributes( + description: 'The merge request is reviewed.', + value: 'reviewed' + ), + 'UNREVIEWED' => have_attributes( + description: 'The merge request is unreviewed.', + value: 'unreviewed' + ) + ) + end +end diff --git a/spec/graphql/types/merge_requests/reviewer_type_spec.rb b/spec/graphql/types/merge_requests/reviewer_type_spec.rb new file mode 100644 index 00000000000..c2182e9968c --- /dev/null +++ b/spec/graphql/types/merge_requests/reviewer_type_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MergeRequestReviewer'] do + specify { expect(described_class).to require_graphql_authorizations(:read_user) } + + it 'has the expected fields' do + expected_fields = %w[ + id + bot + user_permissions + snippets + name + username + email + publicEmail + avatarUrl + webUrl + webPath + todos + state + status + location + authoredMergeRequests + assignedMergeRequests + reviewRequestedMergeRequests + groupMemberships + groupCount + projectMemberships + starredProjects + callouts + merge_request_interaction + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + describe '#merge_request_interaction' do + subject { described_class.fields['mergeRequestInteraction'] } + + it 'returns the correct type' do + is_expected.to have_graphql_type(Types::UserMergeRequestInteractionType) + end + + it 'has the correct arguments' do + is_expected.to have_attributes(arguments: be_empty) + end + end +end diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb index 806495250ac..5c2ae5cea3c 100644 --- a/spec/graphql/types/milestone_type_spec.rb +++ b/spec/graphql/types/milestone_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do it 'has the expected fields' do expected_fields = %w[ - id title description state web_path + id iid title description state web_path due_date start_date created_at updated_at project_milestone group_milestone subgroup_milestone stats diff --git a/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb b/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb new file mode 100644 index 00000000000..18b17286654 --- /dev/null +++ b/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ConanFileMetadata'] do + it 'includes conan file metadatum fields' do + expected_fields = %w[ + id created_at updated_at recipe_revision package_revision conan_package_reference conan_file_type + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb b/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb new file mode 100644 index 00000000000..379cb5168a8 --- /dev/null +++ b/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ConanMetadatumFileTypeEnum'] do + it 'uses all possible options from model' do + expected_keys = ::Packages::Conan::FileMetadatum.conan_file_types + .keys + .map(&:upcase) + + expect(described_class.values.keys).to contain_exactly(*expected_keys) + end +end diff --git a/spec/graphql/types/packages/conan/metadatum_type_spec.rb b/spec/graphql/types/packages/conan/metadatum_type_spec.rb new file mode 100644 index 00000000000..f8f24ffc95a --- /dev/null +++ b/spec/graphql/types/packages/conan/metadatum_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ConanMetadata'] do + it 'includes conan metadatum fields' do + expected_fields = %w[ + id created_at updated_at package_username package_channel recipe recipe_path + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/packages/package_without_versions_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb index faa79e588d5..06093813315 100644 --- a/spec/graphql/types/packages/package_without_versions_type_spec.rb +++ b/spec/graphql/types/packages/package_details_type_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' -RSpec.describe GitlabSchema.types['PackageWithoutVersions'] do +RSpec.describe GitlabSchema.types['PackageDetailsType'] do it 'includes all the package fields' do expected_fields = %w[ - id name version created_at updated_at package_type tags project pipelines + id name version created_at updated_at package_type tags project pipelines versions package_files ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/packages/package_file_type_spec.rb b/spec/graphql/types/packages/package_file_type_spec.rb new file mode 100644 index 00000000000..8e20aea5220 --- /dev/null +++ b/spec/graphql/types/packages/package_file_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PackageFile'] do + it 'includes package file fields' do + expected_fields = %w[ + id file_name created_at updated_at size file_name download_path file_md5 file_sha1 file_sha256 file_metadata + ] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb index 43289a019b3..544d6ddc3af 100644 --- a/spec/graphql/types/packages/package_type_spec.rb +++ b/spec/graphql/types/packages/package_type_spec.rb @@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['Package'] do id name version package_type created_at updated_at project - tags pipelines versions + tags pipelines metadata versions ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 9579ef8b99b..f2c4068f048 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -106,7 +106,8 @@ RSpec.describe GitlabSchema.types['Project'] do expect(secure_analyzers_prefix['type']).to eq('string') expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX') expect(secure_analyzers_prefix['label']).to eq('Image prefix') - expect(secure_analyzers_prefix['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers') + expect(secure_analyzers_prefix['defaultValue']) + .to eq('registry.gitlab.com/gitlab-org/security-products/analyzers') expect(secure_analyzers_prefix['value']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers') expect(secure_analyzers_prefix['size']).to eq('LARGE') expect(secure_analyzers_prefix['options']).to be_nil @@ -124,8 +125,8 @@ RSpec.describe GitlabSchema.types['Project'] do it "returns the project's sast configuration for analyzer variables" do analyzer = subject.dig('data', 'project', 'sastCiConfiguration', 'analyzers', 'nodes').first - expect(analyzer['name']).to eq('brakeman') - expect(analyzer['label']).to eq('Brakeman') + expect(analyzer['name']).to eq('bandit') + expect(analyzer['label']).to eq('Bandit') expect(analyzer['enabled']).to eq(true) end @@ -184,9 +185,11 @@ RSpec.describe GitlabSchema.types['Project'] do context 'when repository is accessible only by team members' do it "returns no configuration" do - project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED, - builds_access_level: ProjectFeature::DISABLED, - repository_access_level: ProjectFeature::PRIVATE) + project.project_feature.update!( + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED, + repository_access_level: ProjectFeature::PRIVATE + ) secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration') expect(secure_analyzers_prefix).to be_nil @@ -240,6 +243,7 @@ RSpec.describe GitlabSchema.types['Project'] do :assignee_username, :reviewer_username, :milestone_title, + :not, :sort ) end @@ -342,8 +346,13 @@ RSpec.describe GitlabSchema.types['Project'] do let_it_be(:project) { create(:project, :public) } context 'when project has Jira imports' do - let_it_be(:jira_import1) { create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago) } - let_it_be(:jira_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) } + let_it_be(:jira_import1) do + create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago) + end + + let_it_be(:jira_import2) do + create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) + end it 'retrieves the imports' do expect(subject).to contain_exactly(jira_import1, jira_import2) @@ -363,4 +372,11 @@ RSpec.describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_type(Types::Ci::AnalyticsType) } it { is_expected.to have_graphql_resolver(Resolvers::ProjectPipelineStatisticsResolver) } end + + describe 'jobs field' do + subject { described_class.fields['jobs'] } + + it { is_expected.to have_graphql_type(Types::Ci::JobType.connection_type) } + it { is_expected.to have_graphql_arguments(:statuses) } + end end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index cb8e875dbf4..d3dcdd260b0 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -98,6 +98,6 @@ RSpec.describe GitlabSchema.types['Query'] do describe 'package field' do subject { described_class.fields['package'] } - it { is_expected.to have_graphql_type(Types::Packages::PackageType) } + it { is_expected.to have_graphql_type(Types::Packages::PackageDetailsType) } end end diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb new file mode 100644 index 00000000000..f8647e4e964 --- /dev/null +++ b/spec/graphql/types/repository/blob_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Repository::BlobType do + specify { expect(described_class.graphql_name).to eq('RepositoryBlob') } + + specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) } +end diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb index e9199bd286e..fa1e54dfcfa 100644 --- a/spec/graphql/types/repository_type_spec.rb +++ b/spec/graphql/types/repository_type_spec.rb @@ -12,4 +12,8 @@ RSpec.describe GitlabSchema.types['Repository'] do specify { expect(described_class).to have_graphql_field(:tree) } specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) } + + specify { expect(described_class).to have_graphql_field(:blobs) } + + specify { expect(described_class).to have_graphql_field(:branch_names, calls_gitaly?: true, complexity: 170) } end diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb index 4d827186a9b..b87770ebe8d 100644 --- a/spec/graphql/types/snippet_type_spec.rb +++ b/spec/graphql/types/snippet_type_spec.rb @@ -161,6 +161,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do describe '#blobs' do let_it_be(:snippet) { create(:personal_snippet, :public, author: user) } + let(:query_blobs) { subject.dig('data', 'snippets', 'nodes')[0].dig('blobs', 'nodes') } let(:paths) { [] } let(:query) do @@ -201,6 +202,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do context 'when snippet has repository' do let_it_be(:snippet) { create(:personal_snippet, :repository, :public, author: user) } + let(:blobs) { snippet.blobs } it_behaves_like 'an array' diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb new file mode 100644 index 00000000000..38bd70d5097 --- /dev/null +++ b/spec/graphql/types/timelog_type_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['Timelog'] do + let(:fields) { %i[spent_at time_spent user issue note] } + + it { expect(described_class.graphql_name).to eq('Timelog') } + it { expect(described_class).to have_graphql_fields(fields) } + it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) } + + describe 'user field' do + subject { described_class.fields['user'] } + + it 'returns user' do + is_expected.to have_non_null_graphql_type(Types::UserType) + end + end + + describe 'issue field' do + subject { described_class.fields['issue'] } + + it 'returns issue' do + is_expected.to have_graphql_type(Types::IssueType) + end + end + + describe 'note field' do + subject { described_class.fields['note'] } + + it 'returns note' do + is_expected.to have_graphql_type(Types::Notes::NoteType) + end + end +end diff --git a/spec/graphql/types/user_merge_request_interaction_type_spec.rb b/spec/graphql/types/user_merge_request_interaction_type_spec.rb new file mode 100644 index 00000000000..f424c9200ab --- /dev/null +++ b/spec/graphql/types/user_merge_request_interaction_type_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let(:interaction) { ::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset) } + + specify { expect(described_class).to require_graphql_authorizations(:read_merge_request) } + + it 'has the expected fields' do + expected_fields = %w[ + can_merge + can_update + review_state + reviewed + approved + ] + + expect(described_class).to have_graphql_fields(*expected_fields).at_least + end + + def resolve(field_name) + resolve_field(field_name, interaction, current_user: current_user) + end + + describe '#can_merge' do + subject { resolve(:can_merge) } + + context 'when the user cannot merge' do + it { is_expected.to be false } + end + + context 'when the user can merge' do + before do + project.add_maintainer(user) + end + + it { is_expected.to be true } + end + end + + describe '#can_update' do + subject { resolve(:can_update) } + + context 'when the user cannot update the MR' do + it { is_expected.to be false } + end + + context 'when the user can update the MR' do + before do + project.add_developer(user) + end + + it { is_expected.to be true } + end + end + + describe '#review_state' do + subject { resolve(:review_state) } + + context 'when the user has not been asked to review the MR' do + it { is_expected.to be_nil } + + it 'implies not reviewed' do + expect(resolve(:reviewed)).to be false + end + end + + context 'when the user has been asked to review the MR' do + before do + merge_request.reviewers << user + end + + it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) } + + it 'implies not reviewed' do + expect(resolve(:reviewed)).to be false + end + end + + context 'when the user has provided a review' do + before do + merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed']) + end + + it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) } + + it 'implies reviewed' do + expect(resolve(:reviewed)).to be true + end + end + end + + describe '#approved' do + subject { resolve(:approved) } + + context 'when the user has not approved the MR' do + it { is_expected.to be false } + end + + context 'when the user has approved the MR' do + before do + merge_request.approved_by_users << user + end + + it { is_expected.to be true } + end + end +end |