diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index dbbfdd76fe8bfe6994aaf2a3f5315f8d9e20b037..ff3a09a2d2d89a44ddf41169d1f252c224894da1 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -59,7 +59,7 @@ def pause private def runner - @runner ||= Ci::RunnersFinder.new(current_user: current_user, group: @group, params: {}).execute + @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute .except(:limit, :offset) .find(params[:id]) end diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 0f40c9bfd2cb8f422a9694080de22b6a3463260b..a290ef9b5e7b84bc91ddae699991d457969ef883 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -17,7 +17,7 @@ class CiCdController < Groups::ApplicationController NUMBER_OF_RUNNERS_PER_PAGE = 4 def show - runners_finder = Ci::RunnersFinder.new(current_user: current_user, group: @group, params: params) + runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group })) # We need all runners for count @all_group_runners = runners_finder.execute.except(:limit, :offset) @group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb index d34b32024335a78e1e5443611cf9b44884fad96a..8bc2a47a024fd9fc13b66a668fb0db04e7e4d92c 100644 --- a/app/finders/ci/runners_finder.rb +++ b/app/finders/ci/runners_finder.rb @@ -7,9 +7,9 @@ class RunnersFinder < UnionFinder ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze DEFAULT_SORT = 'created_at_desc' - def initialize(current_user:, group: nil, params:) + def initialize(current_user:, params:) @params = params - @group = group + @group = params.delete(:group) @current_user = current_user end @@ -48,10 +48,16 @@ def all_runners def group_runners raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group) - # Getting all runners from the group itself and all its descendants - descendant_projects = Project.for_group_and_its_subgroups(@group) - - @runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects) + @runners = case @params[:membership] + when :direct + Ci::Runner.belonging_to_group(@group.id) + when :descendants, nil + # Getting all runners from the group itself and all its descendant groups/projects + descendant_projects = Project.for_group_and_its_subgroups(@group) + Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects) + else + raise ArgumentError, 'Invalid membership filter' + end end def filter_by_status! diff --git a/app/graphql/resolvers/ci/group_runners_resolver.rb b/app/graphql/resolvers/ci/group_runners_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..e9c399d3855bb58cbe3dbbc848d57a24a0c9b5e7 --- /dev/null +++ b/app/graphql/resolvers/ci/group_runners_resolver.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class GroupRunnersResolver < RunnersResolver + type Types::Ci::RunnerType.connection_type, null: true + + argument :membership, ::Types::Ci::RunnerMembershipFilterEnum, + required: false, + default_value: :descendants, + description: 'Control which runners to include in the results.' + + protected + + def runners_finder_params(params) + super(params).merge(membership: params[:membership]) + end + + def parent_param + raise 'Expected group missing' unless parent.is_a?(Group) + + { group: parent } + end + end + end +end diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb index 1957c4ec058deeb82c842975f8abab021270cc29..07105701daa3e3a6c2856787259c614cbe80f09e 100644 --- a/app/graphql/resolvers/ci/runners_resolver.rb +++ b/app/graphql/resolvers/ci/runners_resolver.rb @@ -34,7 +34,7 @@ def resolve_with_lookahead(**args) .execute) end - private + protected def runners_finder_params(params) { @@ -47,6 +47,19 @@ def runners_finder_params(params) tag_name: node_selection&.selects?(:tag_list) } }.compact + .merge(parent_param) + end + + def parent_param + return {} unless parent + + raise "Unexpected parent type: #{parent.class}" + end + + private + + def parent + object.respond_to?(:sync) ? object.sync : object end end end diff --git a/app/graphql/types/ci/runner_membership_filter_enum.rb b/app/graphql/types/ci/runner_membership_filter_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..2e1051b21518bab724d5596e2251eb06740374e3 --- /dev/null +++ b/app/graphql/types/ci/runner_membership_filter_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Ci + class RunnerMembershipFilterEnum < BaseEnum + graphql_name 'RunnerMembershipFilter' + description 'Values for filtering runners in namespaces.' + + value 'DIRECT', + description: "Include runners that have a direct relationship.", + value: :direct + + value 'DESCENDANTS', + description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).", + value: :descendants + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index fbf0084cd0e82e6ec963418c636164b7fc255cfa..baf0fa80fc367a201b7b01dd2f2b56e3804e5bb0 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -155,6 +155,12 @@ def label(title:) complexity: 5, resolver: Resolvers::GroupsResolver + field :runners, Types::Ci::RunnerType.connection_type, + null: true, + resolver: Resolvers::Ci::GroupRunnersResolver, + description: "Find runners visible to the current user.", + feature_flag: :runner_graphql_query + def avatar_url object.avatar_url(only_path: false) end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6600901e187ba6114dd64b74b607df3494ebfff3..a9c2a7b522c48711a36fdafd466e50e30eb9a986 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10004,6 +10004,27 @@ four standard [pagination arguments](#connection-pagination-arguments): | `search` | [`String`](#string) | Search project with most similar names or paths. | | `sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. | +##### `Group.runners` + +Find runners visible to the current user. Available only when feature flag `runner_graphql_query` is enabled. This flag is enabled by default. + +Returns [`CiRunnerConnection`](#cirunnerconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `membership` | [`RunnerMembershipFilter`](#runnermembershipfilter) | Control which runners to include in the results. | +| `search` | [`String`](#string) | Filter by full token or partial text in description field. | +| `sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. | +| `status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. | +| `tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). | +| `type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. | + ##### `Group.timelogs` Time logged on issues and merge requests in the group and its subgroups. @@ -15626,6 +15647,15 @@ Status of a requirement based on last test report. | `MISSING` | Requirements without any test report. | | `PASSED` | | +### `RunnerMembershipFilter` + +Values for filtering runners in namespaces. + +| Value | Description | +| ----- | ----------- | +| `DESCENDANTS` | Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried). | +| `DIRECT` | Include runners that have a direct relationship. | + ### `SastUiComponentSize` Size of UI component in SAST configuration page. diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb index 599b4ffb8046c240b9b0512a781341dc878e012f..10d3f641e02bf68a8ea5fa39513afa72153f64ab 100644 --- a/spec/finders/ci/runners_finder_spec.rb +++ b/spec/finders/ci/runners_finder_spec.rb @@ -18,6 +18,13 @@ end end + context 'with nil group' do + it 'returns all runners' do + expect(Ci::Runner).to receive(:with_tags).and_call_original + expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2] + end + end + context 'with preload param set to :tag_name true' do it 'requests tags' do expect(Ci::Runner).to receive(:with_tags).and_call_original @@ -158,6 +165,7 @@ let_it_be(:project_4) { create(:project, group: sub_group_2) } let_it_be(:project_5) { create(:project, group: sub_group_3) } let_it_be(:project_6) { create(:project, group: sub_group_4) } + let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) } let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago) } let_it_be(:runner_sub_group_1) { create(:ci_runner, :group, active: false, contacted_at: 11.minutes.ago) } let_it_be(:runner_sub_group_2) { create(:ci_runner, :group, contacted_at: 10.minutes.ago) } @@ -171,7 +179,10 @@ let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5])} let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6])} - let(:params) { {} } + let(:target_group) { nil } + let(:membership) { nil } + let(:extra_params) { {} } + let(:params) { { group: target_group, membership: membership }.merge(extra_params).reject { |_, v| v.nil? } } before do group.runners << runner_group @@ -182,65 +193,104 @@ end describe '#execute' do - subject { described_class.new(current_user: user, group: group, params: params).execute } + subject { described_class.new(current_user: user, params: params).execute } + + shared_examples 'membership equal to :descendants' do + it 'returns all descendant runners' do + expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5, + runner_project_4, runner_project_3, runner_project_2, + runner_project_1, runner_sub_group_4, runner_sub_group_3, + runner_sub_group_2, runner_sub_group_1, runner_group]) + end + end context 'with user as group owner' do before do group.add_owner(user) end - context 'passing no params' do - it 'returns all descendant runners' do - expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5, - runner_project_4, runner_project_3, runner_project_2, - runner_project_1, runner_sub_group_4, runner_sub_group_3, - runner_sub_group_2, runner_sub_group_1, runner_group]) + context 'with :group as target group' do + let(:target_group) { group } + + context 'passing no params' do + it_behaves_like 'membership equal to :descendants' end - end - context 'with sort param' do - let(:params) { { sort: 'contacted_asc' } } + context 'with :descendants membership' do + let(:membership) { :descendants } - it 'sorts by specified attribute' do - expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2, - runner_sub_group_3, runner_sub_group_4, runner_project_1, - runner_project_2, runner_project_3, runner_project_4, - runner_project_5, runner_project_6, runner_project_7]) + it_behaves_like 'membership equal to :descendants' end - end - context 'filtering' do - context 'by search term' do - let(:params) { { search: 'runner_project_search' } } + context 'with :direct membership' do + let(:membership) { :direct } + + it 'returns runners belonging to group' do + expect(subject).to eq([runner_group]) + end + end + + context 'with unknown membership' do + let(:membership) { :unsupported } - it 'returns correct runner' do - expect(subject).to eq([runner_project_3]) + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError, 'Invalid membership filter') end end - context 'by status' do - let(:params) { { status_status: 'paused' } } + context 'with nil group' do + let(:target_group) { nil } - it 'returns correct runner' do - expect(subject).to eq([runner_sub_group_1]) + it 'returns no runners' do + # Query should run against all runners, however since user is not admin, query returns no results + expect(subject).to eq([]) end end - context 'by tag_name' do - let(:params) { { tag_name: %w[runner_tag] } } + context 'with sort param' do + let(:extra_params) { { sort: 'contacted_asc' } } - it 'returns correct runner' do - expect(subject).to eq([runner_project_5]) + it 'sorts by specified attribute' do + expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2, + runner_sub_group_3, runner_sub_group_4, runner_project_1, + runner_project_2, runner_project_3, runner_project_4, + runner_project_5, runner_project_6, runner_project_7]) end end - context 'by runner type' do - let(:params) { { type_type: 'project_type' } } + context 'filtering' do + context 'by search term' do + let(:extra_params) { { search: 'runner_project_search' } } + + it 'returns correct runner' do + expect(subject).to eq([runner_project_3]) + end + end + + context 'by status' do + let(:extra_params) { { status_status: 'paused' } } + + it 'returns correct runner' do + expect(subject).to eq([runner_sub_group_1]) + end + end + + context 'by tag_name' do + let(:extra_params) { { tag_name: %w[runner_tag] } } + + it 'returns correct runner' do + expect(subject).to eq([runner_project_5]) + end + end + + context 'by runner type' do + let(:extra_params) { { type_type: 'project_type' } } - it 'returns correct runners' do - expect(subject).to eq([runner_project_7, runner_project_6, - runner_project_5, runner_project_4, - runner_project_3, runner_project_2, runner_project_1]) + it 'returns correct runners' do + expect(subject).to eq([runner_project_7, runner_project_6, + runner_project_5, runner_project_4, + runner_project_3, runner_project_2, runner_project_1]) + end end end end @@ -278,7 +328,7 @@ end describe '#sort_key' do - subject { described_class.new(current_user: user, group: group, params: params).sort_key } + subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key } context 'without params' do it 'returns created_at_desc' do @@ -287,7 +337,7 @@ end context 'with params' do - let(:params) { { sort: 'contacted_asc' } } + let(:extra_params) { { sort: 'contacted_asc' } } it 'returns contacted_asc' do expect(subject).to eq('contacted_asc') diff --git a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..89a2437a189f6580471fcc4f823a6302f7ccf2f9 --- /dev/null +++ b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::GroupRunnersResolver do + include GraphqlHelpers + + describe '#resolve' do + subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) } + + include_context 'runners resolver setup' + + let(:obj) { group } + let(:args) { {} } + + # First, we can do a couple of basic real tests to verify common cases. That ensures that the code works. + context 'when user cannot see runners' do + it 'returns no runners' do + expect(subject.items.to_a).to eq([]) + end + end + + context 'with user as group owner' do + before do + group.add_owner(user) + end + + it 'returns all the runners' do + expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner) + end + + context 'with membership direct' do + let(:args) { { membership: :direct } } + + it 'returns only direct runners' do + expect(subject.items.to_a).to contain_exactly(group_runner) + end + end + end + + # Then, we can check specific edge cases for this resolver + context 'with obj set to nil' do + let(:obj) { nil } + + it 'raises an error' do + expect { subject }.to raise_error('Expected group missing') + end + end + + context 'with obj not set to group' do + let(:obj) { build(:project) } + + it 'raises an error' do + expect { subject }.to raise_error('Expected group missing') + end + end + + # Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice. + # Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back. + describe 'Allowed query arguments' do + let(:finder) { instance_double(::Ci::RunnersFinder) } + let(:args) do + { + status: 'active', + type: :group_type, + tag_list: ['active_runner'], + search: 'abc', + sort: :contacted_asc, + membership: :descendants + } + end + + let(:expected_params) do + { + status_status: 'active', + type_type: :group_type, + tag_name: ['active_runner'], + preload: { tag_name: nil }, + search: 'abc', + sort: 'contacted_asc', + membership: :descendants, + group: group + } + end + + it 'calls RunnersFinder with expected arguments' do + allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder) + allow(finder).to receive(:execute).once.and_return([:execute_return_value]) + + expect(subject.items.to_a).to eq([:execute_return_value]) + end + end + end +end diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb index 5ac15d5729ffd095042a36c7d177fa5ae1b841ab..bb8dadeca40cf817eef8bca3520a74024a30d081 100644 --- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb @@ -5,185 +5,70 @@ RSpec.describe Resolvers::Ci::RunnersResolver do include GraphqlHelpers - let_it_be(:user) { create_default(:user, :admin) } - let_it_be(:group) { create(:group, :public) } - let_it_be(:project) { create(:project, :repository, :public) } - - let_it_be(:inactive_project_runner) do - create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner)) - end - - let_it_be(:offline_project_runner) do - create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner)) - end - - let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 1.second.ago) } - let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } - describe '#resolve' do - subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a } - - let(:args) do - {} - end - - context 'when the user cannot see runners' do - let(:user) { create(:user) } - - it 'returns no runners' do - is_expected.to be_empty - end - end - - context 'without sort' do - it 'returns all the runners' do - is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner) - end - end - - context 'with a sort argument' do - context "set to :contacted_asc" do - let(:args) do - { sort: :contacted_asc } - end - - it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner]) } - end - - context "set to :contacted_desc" do - let(:args) do - { sort: :contacted_desc } - end - - it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner].reverse) } - end - - context "set to :created_at_desc" do - let(:args) do - { sort: :created_at_desc } - end + let(:obj) { nil } + let(:args) { {} } - it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner]) } - end - - context "set to :created_at_asc" do - let(:args) do - { sort: :created_at_asc } - end - - it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner].reverse) } - end - end + subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) } - context 'when type is filtered' do - let(:args) do - { type: runner_type.to_s } - end + include_context 'runners resolver setup' - context 'to instance runners' do - let(:runner_type) { :instance_type } + # First, we can do a couple of basic real tests to verify common cases. That ensures that the code works. + context 'when user cannot see runners' do + let(:user) { build(:user) } - it 'returns the instance runner' do - is_expected.to contain_exactly(instance_runner) - end - end - - context 'to group runners' do - let(:runner_type) { :group_type } - - it 'returns the group runner' do - is_expected.to contain_exactly(group_runner) - end - end - - context 'to project runners' do - let(:runner_type) { :project_type } - - it 'returns the project runner' do - is_expected.to contain_exactly(inactive_project_runner, offline_project_runner) - end + it 'returns no runners' do + expect(subject.items.to_a).to eq([]) end end - context 'when status is filtered' do - let(:args) do - { status: runner_status.to_s } - end - - context 'to active runners' do - let(:runner_status) { :active } - - it 'returns the instance and group runners' do - is_expected.to contain_exactly(offline_project_runner, group_runner, instance_runner) - end - end - - context 'to offline runners' do - let(:runner_status) { :offline } + context 'when user can see runners' do + let(:obj) { nil } - it 'returns the offline project runner' do - is_expected.to contain_exactly(offline_project_runner) - end + it 'returns all the runners' do + expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner) end end - context 'when tag list is filtered' do - let(:args) do - { tag_list: tag_list } - end - - context 'with "project_runner" tag' do - let(:tag_list) { ['project_runner'] } + # Then, we can check specific edge cases for this resolver + context 'with obj not set to nil' do + let(:obj) { build(:project) } - it 'returns the project_runner runners' do - is_expected.to contain_exactly(offline_project_runner, inactive_project_runner) - end - end - - context 'with "project_runner" and "active_runner" tags as comma-separated string' do - let(:tag_list) { ['project_runner,active_runner'] } - - it 'returns the offline_project_runner runner' do - is_expected.to contain_exactly(offline_project_runner) - end - end - - context 'with "active_runner" and "instance_runner" tags as array' do - let(:tag_list) { %w[instance_runner active_runner] } - - it 'returns the offline_project_runner runner' do - is_expected.to contain_exactly(instance_runner) - end + it 'raises an error' do + expect { subject }.to raise_error(a_string_including('Unexpected parent type')) end end - context 'when text is filtered' do + # Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice. + # Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back. + describe 'Allowed query arguments' do + let(:finder) { instance_double(::Ci::RunnersFinder) } let(:args) do - { search: search_term } - end - - context 'to "project"' do - let(:search_term) { 'project' } - - it 'returns both project runners' do - is_expected.to contain_exactly(inactive_project_runner, offline_project_runner) - end - end - - context 'to "group"' do - let(:search_term) { 'group' } - - it 'returns group runner' do - is_expected.to contain_exactly(group_runner) - end - end - - context 'to "defghi"' do - let(:search_term) { 'defghi' } - - it 'returns runners containing term in token' do - is_expected.to contain_exactly(offline_project_runner) - end + { + status: 'active', + type: :instance_type, + tag_list: ['active_runner'], + search: 'abc', + sort: :contacted_asc + } + end + + let(:expected_params) do + { + status_status: 'active', + type_type: :instance_type, + tag_name: ['active_runner'], + preload: { tag_name: nil }, + search: 'abc', + sort: 'contacted_asc' + } + end + + it 'calls RunnersFinder with expected arguments' do + allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder) + allow(finder).to receive(:execute).once.and_return([:execute_return_value]) + + expect(subject.items.to_a).to eq([:execute_return_value]) end end end diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..aa857cfdb703d08f1395d5659feb6b0a186f0294 --- /dev/null +++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_context 'runners resolver setup' do + let_it_be(:user) { create_default(:user, :admin) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:subgroup) { create(:group, :public, parent: group) } + let_it_be(:project) { create(:project, :public, group: group) } + + let_it_be(:inactive_project_runner) do + create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner)) + end + + let_it_be(:offline_project_runner) do + create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner)) + end + + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) } + let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) } + let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } +end