diff --git a/app/finders/organizations/groups_finder.rb b/app/finders/organizations/groups_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b59a3106a3a62adf9cb4228fccc60f2fef93b5e
--- /dev/null
+++ b/app/finders/organizations/groups_finder.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# Organizations::GroupsFinder
+#
+# Used to find Groups within an Organization
+module Organizations
+ class GroupsFinder
+ # @param organization [Organizations::Organization]
+ # @param current_user [User]
+ # @param params [{ sort: { field: [String], direction: [String] }, search: [String] }]
+ def initialize(organization:, current_user:, params: {})
+ @organization = organization
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return Group.none if organization.nil? || !authorized?
+
+ filter_groups(all_accessible_groups)
+ .then { |groups| sort(groups) }
+ .then(&:with_route)
+ end
+
+ private
+
+ attr_reader :organization, :params, :current_user
+
+ def all_accessible_groups
+ current_user.authorized_groups.in_organization(organization)
+ end
+
+ def filter_groups(groups)
+ by_search(groups)
+ end
+
+ def by_search(groups)
+ return groups unless params[:search].present?
+
+ groups.search(params[:search])
+ end
+
+ def sort(groups)
+ return default_sort_order(groups) if params[:sort].blank?
+
+ field = params[:sort][:field]
+ direction = params[:sort][:direction]
+ groups.reorder(field => direction) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def default_sort_order(groups)
+ groups.sort_by_attribute('name_asc')
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_organization, organization)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/groups_resolver.rb b/app/graphql/resolvers/organizations/groups_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f50713b9b439aa5eb9ff878ad9aeeb4e34aae80
--- /dev/null
+++ b/app/graphql/resolvers/organizations/groups_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class GroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesGroups
+
+ type Types::GroupType.connection_type, null: true
+
+ authorize :read_group
+
+ argument :search,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Search query for group name or full path.',
+ alpha: { milestone: '16.4' }
+
+ argument :sort,
+ Types::Organizations::GroupSortEnum,
+ description: 'Criteria to sort organization groups by.',
+ required: false,
+ default_value: { field: 'name', direction: :asc },
+ alpha: { milestone: '16.4' }
+
+ private
+
+ def resolve_groups(**args)
+ return Group.none if Feature.disabled?(:resolve_organization_groups, context[:current_user])
+
+ ::Organizations::GroupsFinder
+ .new(organization: object, current_user: context[:current_user], params: args)
+ .execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/organization_resolver.rb b/app/graphql/resolvers/organizations/organization_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9194d9a32c5dd9de8834ec1e46e528bf5ea7b487
--- /dev/null
+++ b/app/graphql/resolvers/organizations/organization_resolver.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class OrganizationResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :read_organization
+
+ type Types::Organizations::OrganizationType, null: true
+
+ argument :id,
+ Types::GlobalIDType[::Organizations::Organization],
+ required: true,
+ description: 'ID of the organization.'
+
+ def resolve(id:)
+ authorized_find!(id: id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/group_sort_enum.rb b/app/graphql/types/organizations/group_sort_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8fb2f553539a2d1bd222538427317d840c78ff01
--- /dev/null
+++ b/app/graphql/types/organizations/group_sort_enum.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class GroupSortEnum < BaseEnum
+ graphql_name 'OrganizationGroupSort'
+ description 'Values for sorting organization groups'
+
+ sortable_fields = ['ID', 'Name', 'Path', 'Updated at', 'Created at']
+
+ sortable_fields.each do |field|
+ value "#{field.upcase.tr(' ', '_')}_ASC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :asc },
+ description: "#{field} in ascending order.",
+ alpha: { milestone: '16.4' }
+
+ value "#{field.upcase.tr(' ', '_')}_DESC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :desc },
+ description: "#{field} in descending order.",
+ alpha: { milestone: '16.4' }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/organization_type.rb b/app/graphql/types/organizations/organization_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..791fddc52667dfb6d4dbbf192411443c51442c16
--- /dev/null
+++ b/app/graphql/types/organizations/organization_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class OrganizationType < BaseObject
+ graphql_name 'Organization'
+
+ authorize :read_organization
+
+ field :groups,
+ Types::GroupType.connection_type,
+ null: false,
+ description: 'Groups within this organization that the user has access to.',
+ alpha: { milestone: '16.4' },
+ resolver: ::Resolvers::Organizations::GroupsResolver
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the organization.',
+ alpha: { milestone: '16.4' }
+ field :name,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Name of the organization.',
+ alpha: { milestone: '16.4' }
+ field :path,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Path of the organization.',
+ alpha: { milestone: '16.4' }
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 38b8973034d3b82b4a6c87338bc257c3ff679b52..e3dd02110291d71f79b01c4347109ad7b9846e99 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -96,6 +96,12 @@ class QueryType < ::Types::BaseObject
required: true,
description: 'Global ID of the note.'
end
+ field :organization,
+ Types::Organizations::OrganizationType,
+ null: true,
+ resolver: Resolvers::Organizations::OrganizationResolver,
+ description: "Find an organization.",
+ alpha: { milestone: '16.4' }
field :package,
description: 'Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.',
resolver: Resolvers::PackageDetailsResolver
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index abf4ffaead492416d45c55ce73bef4f25ae4ce66..a81d0f047ba1a28b98589edb66f4e859c5327a52 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -166,6 +166,7 @@ class Namespace < ApplicationRecord
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
+ scope :in_organization, -> (organization) { where(organization: organization) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 9f2119949fba21c55dfceb5f00ed21d36894cca6..489fd6e0da7c19871df14f43689ab3bb1fa1518d 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -38,7 +38,7 @@ def to_param
end
def user?(user)
- users.exists?(user.id)
+ organization_users.exists?(user: user)
end
private
diff --git a/config/feature_flags/development/resolve_organization_groups.yml b/config/feature_flags/development/resolve_organization_groups.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a70c8568a621163c6aaa5ae90f7f336b70e329c
--- /dev/null
+++ b/config/feature_flags/development/resolve_organization_groups.yml
@@ -0,0 +1,8 @@
+---
+name: resolve_organization_groups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128733
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421673
+milestone: '16.3'
+type: development
+group: group::tenant scale
+default_enabled: true
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index aa679dad425e021535518cfa375fa877102c1ade..ff5cdee4009d421b0d5ca534eccb5c0ebb679c61 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -532,6 +532,22 @@ Returns [`Note`](#note).
| ---- | ---- | ----------- |
| `id` | [`NoteID!`](#noteid) | Global ID of the note. |
+### `Query.organization`
+
+Find an organization.
+
+WARNING:
+**Introduced** in 16.4.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`Organization`](#organization).
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `id` | [`OrganizationsOrganizationID!`](#organizationsorganizationid) | ID of the organization. |
+
### `Query.package`
Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.
@@ -20221,6 +20237,39 @@ Active period time range for on-call rotation.
| `endTime` | [`String`](#string) | End of the rotation active period. |
| `startTime` | [`String`](#string) | Start of the rotation active period. |
+### `Organization`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID of the organization. |
+| `name` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name of the organization. |
+| `path` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path of the organization. |
+
+#### Fields with arguments
+
+##### `Organization.groups`
+
+Groups within this organization that the user has access to.
+
+WARNING:
+**Introduced** in 16.4.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`GroupConnection!`](#groupconnection).
+
+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 |
+| ---- | ---- | ----------- |
+| `search` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Search query for group name or full path. |
+| `sort` **{warning-solid}** | [`OrganizationGroupSort`](#organizationgroupsort) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Criteria to sort organization groups by. |
+
### `OrganizationStateCounts`
Represents the total number of organizations for the represented states.
@@ -27215,6 +27264,23 @@ Rotation length unit of an on-call rotation.
| `HOURS` | Hours. |
| `WEEKS` | Weeks. |
+### `OrganizationGroupSort`
+
+Values for sorting organization groups.
+
+| Value | Description |
+| ----- | ----------- |
+| `CREATED_AT_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Created at in ascending order. |
+| `CREATED_AT_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Created at in descending order. |
+| `ID_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID in ascending order. |
+| `ID_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID in descending order. |
+| `NAME_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name in ascending order. |
+| `NAME_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name in descending order. |
+| `PATH_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path in ascending order. |
+| `PATH_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path in descending order. |
+| `UPDATED_AT_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Updated at in ascending order. |
+| `UPDATED_AT_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Updated at in descending order. |
+
### `OrganizationSort`
Values for sorting organizations.
@@ -28688,6 +28754,12 @@ A `NoteableID` is a global ID. It is encoded as a string.
An example `NoteableID` is: `"gid://gitlab/Noteable/1"`.
+### `OrganizationsOrganizationID`
+
+A `OrganizationsOrganizationID` is a global ID. It is encoded as a string.
+
+An example `OrganizationsOrganizationID` is: `"gid://gitlab/Organizations::Organization/1"`.
+
### `PackagesConanFileMetadatumID`
A `PackagesConanFileMetadatumID` is a global ID. It is encoded as a string.
diff --git a/ee/spec/graphql/types/query_type_spec.rb b/ee/spec/graphql/types/query_type_spec.rb
index 0ac790cdc8be6332d754ade11c9b847251289237..623a4d3289d7b03eaad3f7f32e7ac11abd345dee 100644
--- a/ee/spec/graphql/types/query_type_spec.rb
+++ b/ee/spec/graphql/types/query_type_spec.rb
@@ -19,6 +19,7 @@
:instance_security_dashboard,
:iteration,
:license_history_entries,
+ :organization,
:subscription_future_entries,
:vulnerabilities,
:vulnerabilities_count_by_day,
diff --git a/spec/finders/organizations/groups_finder_spec.rb b/spec/finders/organizations/groups_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..972d80ab036789fdde05dcd1a8226a708c849572
--- /dev/null
+++ b/spec/finders/organizations/groups_finder_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::GroupsFinder, feature_category: :cell do
+ include AdminModeHelper
+
+ let(:current_user) { user }
+ let(:params) { {} }
+ let(:finder) { described_class.new(organization: organization, current_user: current_user, params: params) }
+
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:user) { organization_user.user }
+ let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) }
+ let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) }
+ let_it_be(:outside_organization_group) { create(:group) }
+ let_it_be(:private_group) do
+ create(:group, :private, name: 'private-group', organization: organization)
+ end
+
+ let_it_be(:no_access_group_in_org) do
+ create(:group, :private, name: 'no-access', organization: organization)
+ end
+
+ before_all do
+ private_group.add_developer(user)
+ public_group.add_developer(user)
+ other_group.add_developer(user)
+ outside_organization_group.add_developer(user)
+ end
+
+ subject(:result) { finder.execute.to_a }
+
+ describe '#execute' do
+ context 'when user is not authorized to read the organization' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when organization is nil' do
+ let(:finder) { described_class.new(organization: nil, current_user: current_user, params: params) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when user is authorized to read the organization' do
+ it 'return all accessible groups' do
+ expect(result).to contain_exactly(public_group, private_group, other_group)
+ end
+
+ context 'when search param is passed' do
+ let(:params) { { search: 'the' } }
+
+ it 'filters the groups by search' do
+ expect(result).to contain_exactly(other_group)
+ end
+ end
+
+ context 'when sort param is not passed' do
+ it 'return groups sorted by name in ascending order by default' do
+ expect(result).to eq([other_group, private_group, public_group])
+ end
+ end
+
+ context 'when sort param is passed' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:field, :direction, :sorted_groups) do
+ 'name' | 'asc' | lazy { [other_group, private_group, public_group] }
+ 'name' | 'desc' | lazy { [public_group, private_group, other_group] }
+ 'path' | 'asc' | lazy { [other_group, private_group, public_group] }
+ 'path' | 'desc' | lazy { [public_group, private_group, other_group] }
+ end
+
+ with_them do
+ let(:params) { { sort: { field: field, direction: direction } } }
+ it 'sorts the groups' do
+ expect(result).to eq(sorted_groups)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/organizations/group_sort_enum_spec.rb b/spec/graphql/types/organizations/group_sort_enum_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..57915d95c45e168392004a3a9c03b3359d5566f9
--- /dev/null
+++ b/spec/graphql/types/organizations/group_sort_enum_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationGroupSort'], feature_category: :cell do
+ let(:sort_values) do
+ %w[
+ ID_ASC
+ ID_DESC
+ NAME_ASC
+ NAME_DESC
+ PATH_ASC
+ PATH_DESC
+ UPDATED_AT_ASC
+ UPDATED_AT_DESC
+ CREATED_AT_ASC
+ CREATED_AT_DESC
+ ]
+ end
+
+ specify { expect(described_class.graphql_name).to eq('OrganizationGroupSort') }
+
+ it 'exposes all the organization groups sort values' do
+ expect(described_class.values.keys).to include(*sort_values)
+ end
+end
diff --git a/spec/graphql/types/organizations/organization_type_spec.rb b/spec/graphql/types/organizations/organization_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..78c5dea73088e5d0801ddfd3c8085eeaa5ab3cb9
--- /dev/null
+++ b/spec/graphql/types/organizations/organization_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Organization'], feature_category: :cell do
+ let(:expected_fields) { %w[groups id name path] }
+
+ specify { expect(described_class.graphql_name).to eq('Organization') }
+ specify { expect(described_class).to require_graphql_authorizations(:read_organization) }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 100ecc94f355aef544588139666234407d51c4d6..8bda738751d66d22ab914a9e8f1a5b6aa485d8b0 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -23,6 +23,16 @@
end
end
+ describe 'organization field' do
+ subject { described_class.fields['organization'] }
+
+ it 'finds organization by path' do
+ is_expected.to have_graphql_arguments(:id)
+ is_expected.to have_graphql_type(Types::Organizations::OrganizationType)
+ is_expected.to have_graphql_resolver(Resolvers::Organizations::OrganizationResolver)
+ end
+ end
+
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d09a53527934d7cfa9edfb3ee3cdaa5681d79ad
--- /dev/null
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting organization information', feature_category: :cell do
+ include GraphqlHelpers
+
+ let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) }
+ let(:current_user) { user }
+ let(:groups) { graphql_data_at(:organization, :groups, :edges, :node) }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ FIELDS
+ end
+
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:user) { organization_user.user }
+ let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) }
+ let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) }
+ let_it_be(:outside_organization_group) { create(:group) }
+
+ let_it_be(:private_group) do
+ create(:group, :private, name: 'private-group', organization: organization)
+ end
+
+ let_it_be(:no_access_group_in_org) do
+ create(:group, :private, name: 'no-access', organization: organization)
+ end
+
+ before_all do
+ private_group.add_developer(user)
+ public_group.add_developer(user)
+ other_group.add_developer(user)
+ outside_organization_group.add_developer(user)
+ end
+
+ subject(:request_organization) { post_graphql(query, current_user: current_user) }
+
+ context 'when the user does not have access to the organization' do
+ let(:current_user) { create(:user) }
+
+ it 'returns the organization as all organizations are public' do
+ request_organization
+
+ expect(graphql_data['organization']['id']).to eq(organization.to_global_id.to_s)
+ end
+ end
+
+ context 'when user has access to the organization' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ request_organization
+ end
+ end
+
+ context 'when resolve_organization_groups feature flag is disabled' do
+ before do
+ stub_feature_flags(resolve_organization_groups: false)
+ end
+
+ it 'returns no groups' do
+ request_organization
+
+ expect(graphql_data['organization']).not_to be_nil
+ expect(graphql_data['organization']['groups']['edges']).to be_empty
+ end
+ end
+
+ context 'with `search` argument' do
+ let(:search) { 'oth' }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups(search: "#{search}") {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+ FIELDS
+ end
+
+ it 'filters groups by name' do
+ request_organization
+
+ expect(groups).to contain_exactly(a_graphql_entity_for(other_group))
+ end
+ end
+
+ context 'with `sort` argument' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:authorized_groups) { [public_group, private_group, other_group] }
+
+ where(:field, :direction, :sorted_groups) do
+ 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) }
+ 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse }
+ 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) }
+ 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse }
+ 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) }
+ 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse }
+ end
+
+ with_them do
+ let(:sort) { "#{field}_#{direction}".upcase }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups(sort: #{sort}) {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ FIELDS
+ end
+
+ it 'sorts the groups' do
+ request_organization
+
+ expect(groups.pluck('id')).to eq(sorted_groups.map(&:to_global_id).map(&:to_s))
+ end
+ end
+ end
+ end
+end