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