Skip to content
Snippets Groups Projects
Verified Commit 08e2a938 authored by Divya Mahadevan's avatar Divya Mahadevan :three: Committed by GitLab
Browse files

Adjustments to add on eligible users API to return filtered results

parent 4a731cdc
No related branches found
No related tags found
2 merge requests!170053Security patch upgrade alert: Only expose to admins 17-4,!166452Adjustments to add on eligible users API to return filtered results
Showing
with 307 additions and 29 deletions
......@@ -1097,7 +1097,9 @@ four standard [pagination arguments](#pagination-arguments):
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryselfmanagedaddoneligibleusersaddonpurchaseids"></a>`addOnPurchaseIds` | [`[GitlabSubscriptionsAddOnPurchaseID!]!`](#gitlabsubscriptionsaddonpurchaseid) | Global IDs of the add on purchases to find assignments for. |
| <a id="queryselfmanagedaddoneligibleusersaddontype"></a>`addOnType` | [`GitlabSubscriptionsAddOnType!`](#gitlabsubscriptionsaddontype) | Type of add on to filter the eligible users by. |
| <a id="queryselfmanagedaddoneligibleusersfilterbyassignedseat"></a>`filterByAssignedSeat` | [`String`](#string) | Filter users list by assigned seat. |
| <a id="queryselfmanagedaddoneligibleuserssearch"></a>`search` | [`String`](#string) | Search the user list. |
| <a id="queryselfmanagedaddoneligibleuserssort"></a>`sort` | [`GitlabSubscriptionsUserSort`](#gitlabsubscriptionsusersort) | Sort the user list. |
 
......@@ -23365,7 +23367,9 @@ four standard [pagination arguments](#pagination-arguments):
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupaddoneligibleusersaddonpurchaseids"></a>`addOnPurchaseIds` | [`[GitlabSubscriptionsAddOnPurchaseID!]!`](#gitlabsubscriptionsaddonpurchaseid) | Global IDs of the add on purchases to find assignments for. |
| <a id="groupaddoneligibleusersaddontype"></a>`addOnType` | [`GitlabSubscriptionsAddOnType!`](#gitlabsubscriptionsaddontype) | Type of add on to filter the eligible users by. |
| <a id="groupaddoneligibleusersfilterbyassignedseat"></a>`filterByAssignedSeat` | [`String`](#string) | Filter users list by assigned seat. |
| <a id="groupaddoneligibleuserssearch"></a>`search` | [`String`](#string) | Search the user list. |
| <a id="groupaddoneligibleuserssort"></a>`sort` | [`GitlabSubscriptionsUserSort`](#gitlabsubscriptionsusersort) | Sort the user list. |
 
......@@ -28125,7 +28129,9 @@ four standard [pagination arguments](#pagination-arguments):
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespaceaddoneligibleusersaddonpurchaseids"></a>`addOnPurchaseIds` | [`[GitlabSubscriptionsAddOnPurchaseID!]!`](#gitlabsubscriptionsaddonpurchaseid) | Global IDs of the add on purchases to find assignments for. |
| <a id="namespaceaddoneligibleusersaddontype"></a>`addOnType` | [`GitlabSubscriptionsAddOnType!`](#gitlabsubscriptionsaddontype) | Type of add on to filter the eligible users by. |
| <a id="namespaceaddoneligibleusersfilterbyassignedseat"></a>`filterByAssignedSeat` | [`String`](#string) | Filter users list by assigned seat. |
| <a id="namespaceaddoneligibleuserssearch"></a>`search` | [`String`](#string) | Search the user list. |
| <a id="namespaceaddoneligibleuserssort"></a>`sort` | [`GitlabSubscriptionsUserSort`](#gitlabsubscriptionsusersort) | Sort the user list. |
 
......@@ -10,17 +10,20 @@ query getAddOnEligibleUsers(
$last: Int
$after: String
$before: String
$filterByAssignedSeat: String
) {
namespace(fullPath: $fullPath) {
id
addOnEligibleUsers(
addOnType: $addOnType
addOnPurchaseIds: $addOnPurchaseIds
sort: $sort
search: $search
first: $first
last: $last
after: $after
before: $before
filterByAssignedSeat: $filterByAssignedSeat
) {
nodes {
id
......
......@@ -9,15 +9,18 @@ query getSelfManagedAddOnEligibleUsers(
$last: Int
$after: String
$before: String
$filterByAssignedSeat: String
) {
selfManagedAddOnEligibleUsers(
addOnType: $addOnType
addOnPurchaseIds: $addOnPurchaseIds
search: $search
sort: $sort
first: $first
last: $last
after: $after
before: $before
filterByAssignedSeat: $filterByAssignedSeat
) {
nodes {
id
......
......@@ -4,11 +4,12 @@ module GitlabSubscriptions
class AddOnEligibleUsersFinder
include Gitlab::Utils::StrongMemoize
def initialize(group, add_on_type:, search_term: nil, sort: nil)
def initialize(group, add_on_type:, add_on_purchase_id: nil, filter_options: {}, sort: nil)
@group = group
@add_on_type = add_on_type
@search_term = search_term
@sort = sort
@add_on_purchase_id = add_on_purchase_id
@filter_options = filter_options
end
def execute
......@@ -24,12 +25,14 @@ def execute
.id_in(members.select(:user_id))
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/426357")
search_term ? users.search(search_term) : users.sort_by_attribute(sort)
users = filter_assigned_users(users) if valid_filter_criteria?
filter_options[:search_term] ? users.search(filter_options[:search_term]) : users.sort_by_attribute(sort)
end
private
attr_reader :group, :add_on_type, :search_term, :sort
attr_reader :group, :add_on_type, :add_on_purchase_id, :filter_options, :sort
def member_relations
[
......@@ -118,5 +121,21 @@ def namespace_ban_cte
def namespace_ban_user_ids
::Namespaces::NamespaceBan.from(namespace_ban_cte.table).select(:user_id) # rubocop:disable CodeReuse/ActiveRecord
end
def valid_filter_criteria?
return false unless add_on_purchase_id.present?
[true, false].include? filter_options[:filter_by_assigned_seat]
end
def filter_assigned_users(collection)
assignments = GitlabSubscriptions::UserAddOnAssignment.for_active_add_on_purchase_ids(add_on_purchase_id)
if filter_options[:filter_by_assigned_seat]
collection.id_in(assignments.select(:user_id))
else
collection.id_not_in(assignments.select(:user_id))
end
end
end
end
......@@ -3,11 +3,12 @@
module GitlabSubscriptions
module SelfManaged
class AddOnEligibleUsersFinder
attr_reader :add_on_type, :search_term, :sort
attr_reader :add_on_type, :add_on_purchase_id, :filter_options, :sort
def initialize(add_on_type:, search_term: nil, sort: nil)
def initialize(add_on_type:, add_on_purchase_id: nil, filter_options: {}, sort: nil)
@add_on_type = add_on_type
@search_term = search_term
@add_on_purchase_id = add_on_purchase_id
@filter_options = filter_options
@sort = sort
end
......@@ -16,7 +17,27 @@ def execute
users = ::User.active.without_bots.without_ghosts
search_term ? users.search(search_term) : users.sort_by_attribute(sort)
users = filter_assigned_users(users) if valid_filter_criteria?
filter_options[:search_term] ? users.search(filter_options[:search_term]) : users.sort_by_attribute(sort)
end
private
def valid_filter_criteria?
return false unless add_on_purchase_id.present?
[true, false].include? filter_options[:filter_by_assigned_seat]
end
def filter_assigned_users(collection)
assignments = GitlabSubscriptions::UserAddOnAssignment.for_active_add_on_purchase_ids(add_on_purchase_id)
if filter_options[:filter_by_assigned_seat]
collection.id_in(assignments.select(:user_id))
else
collection.id_not_in(assignments.select(:user_id))
end
end
end
end
......
......@@ -21,19 +21,36 @@ class AddOnEligibleUsersResolver < BaseResolver
required: true,
description: 'Type of add on to filter the eligible users by.'
argument :add_on_purchase_ids,
type: [::Types::GlobalIDType[::GitlabSubscriptions::AddOnPurchase]],
required: true,
description: 'Global IDs of the add on purchases to find assignments for.',
prepare: ->(global_ids, _ctx) do
GitlabSchema.parse_gids(global_ids, expected_type: ::GitlabSubscriptions::AddOnPurchase).map(&:model_id)
end
argument :filter_by_assigned_seat,
type: GraphQL::Types::String,
required: false,
description: 'Filter users list by assigned seat.'
type ::Types::GitlabSubscriptions::AddOnUserType.connection_type,
null: true
alias_method :namespace, :object
def resolve(add_on_type:, search: nil, sort: nil)
def resolve(add_on_type:, add_on_purchase_ids:, search: nil, sort: nil, filter_by_assigned_seat: nil)
authorize!(namespace)
users = ::GitlabSubscriptions::AddOnEligibleUsersFinder.new(
namespace,
add_on_type: add_on_type,
search_term: search,
sort: sort
filter_options: {
search_term: search,
filter_by_assigned_seat: Gitlab::Utils.to_boolean(filter_by_assigned_seat)
},
sort: sort,
add_on_purchase_id: add_on_purchase_ids.first
).execute
offset_pagination(users)
......
......@@ -24,12 +24,29 @@ class AddOnEligibleUsersResolver < BaseResolver
required: true,
description: 'Type of add on to filter the eligible users by.'
def resolve(add_on_type:, search: nil, sort: nil)
argument :add_on_purchase_ids,
type: [::Types::GlobalIDType[::GitlabSubscriptions::AddOnPurchase]],
required: true,
description: 'Global IDs of the add on purchases to find assignments for.',
prepare: ->(global_ids, _ctx) do
GitlabSchema.parse_gids(global_ids, expected_type: ::GitlabSubscriptions::AddOnPurchase).map(&:model_id)
end
argument :filter_by_assigned_seat,
type: GraphQL::Types::String,
required: false,
description: 'Filter users list by assigned seat.'
def resolve(add_on_type:, add_on_purchase_ids:, search: nil, sort: nil, filter_by_assigned_seat: nil)
authorize!
users = ::GitlabSubscriptions::SelfManaged::AddOnEligibleUsersFinder.new(
add_on_type: add_on_type,
search_term: search,
add_on_purchase_id: add_on_purchase_ids.first,
filter_options: {
search_term: search,
filter_by_assigned_seat: Gitlab::Utils.to_boolean(filter_by_assigned_seat)
},
sort: sort
).execute
......
......@@ -139,7 +139,8 @@
end
it 'filters the eligible users by search term' do
finder = described_class.new(root_namespace, add_on_type: :code_suggestions, search_term: 'Second')
finder = described_class.new(root_namespace, add_on_type: :code_suggestions,
filter_options: { search_term: 'Second' })
expect(finder.execute).to match_array([user_2])
end
......@@ -189,5 +190,65 @@
end
end
end
context 'when supplied a filter option' do
let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro) }
let_it_be(:owner) { create(:user, name: 'Owner User') }
let_it_be(:assigned_user) { create(:user, name: 'Assigned User') }
let_it_be(:non_assigned_user) { create(:user, name: 'Non Assigned User') }
before_all do
root_namespace.add_owner(owner)
subgroup.add_developer(assigned_user)
subgroup.add_developer(non_assigned_user)
add_on_purchase.assigned_users.create!(user: owner)
add_on_purchase.assigned_users.create!(user: assigned_user)
end
context 'when filter_by_assigned_seat is true' do
let(:filter_options) { { filter_by_assigned_seat: true } }
it 'filters users that got assigned seats' do
finder = described_class.new(
root_namespace,
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to match_array([owner, assigned_user])
end
end
context 'when filter_by_assigned_seat is false' do
let(:filter_options) { { filter_by_assigned_seat: false } }
it 'filters users not assigned seats' do
finder = described_class.new(
root_namespace,
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to match_array([non_assigned_user])
end
end
context 'when filter_by_assigned_seat is nil' do
let(:filter_options) { { filter_by_assigned_seat: nil } }
it 'returns all eligible users without filtering' do
finder = described_class.new(
root_namespace,
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to match_array([owner, assigned_user, non_assigned_user])
end
end
end
end
end
......@@ -49,7 +49,10 @@
let(:non_matching_user) { create(:user, name: 'Non') }
it 'filters users by search term if provided' do
finder = described_class.new(add_on_type: :code_suggestions, search_term: 'Matching')
finder = described_class.new(
add_on_type: :code_suggestions,
filter_options: { search_term: 'Matching' }
)
expect(finder.execute).to include(matching_user)
expect(finder.execute).not_to include(non_matching_user)
......@@ -94,5 +97,59 @@
end
end
end
context 'when supplied a filter option' do
let(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, :self_managed, :gitlab_duo_pro) }
let(:assigned_user) { create(:user, name: 'Assigned User') }
let(:non_assigned_user) { create(:user, name: 'Non Assigned User') }
before do
add_on_purchase.assigned_users.create!(user: assigned_user)
end
context 'when filter_by_assigned_seat is true' do
let(:filter_options) { { filter_by_assigned_seat: true } }
it 'filters users by assigned seats' do
finder = described_class.new(
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to include(assigned_user)
expect(finder.execute).not_to include(non_assigned_user)
end
end
context 'when filter_by_assigned_seat is false' do
let(:filter_options) { { filter_by_assigned_seat: false } }
it 'filters users by not assigned seats' do
finder = described_class.new(
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to include(non_assigned_user)
expect(finder.execute).not_to include(assigned_user)
end
end
context 'when filter_by_assigned_seat is nil' do
let(:filter_options) { { filter_by_assigned_seat: nil } }
it 'returns all eligible users without filtering' do
finder = described_class.new(
add_on_purchase_id: add_on_purchase.id,
add_on_type: :code_suggestions,
filter_options: filter_options
)
expect(finder.execute).to include(non_assigned_user, assigned_user)
end
end
end
end
end
......@@ -25,6 +25,8 @@
])
end
let(:add_on_params) { { add_on_type: :CODE_SUGGESTIONS, add_on_purchase_ids: query_add_on_purchase_ids } }
before do
stub_saas_features(gitlab_com_subscriptions: true)
end
......@@ -33,7 +35,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(:addOnEligibleUsers, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:addOnEligibleUsers, add_on_params, query_fields)
)
end
......@@ -56,7 +58,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: subgroup.full_path },
query_graphql_field(:addOnEligibleUsers, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:addOnEligibleUsers, add_on_params, query_fields)
)
end
......@@ -92,7 +94,7 @@
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(
:addOnEligibleUsers,
{ add_on_type: :CODE_SUGGESTIONS, search: 'Group User' },
{ add_on_type: :CODE_SUGGESTIONS, search: 'Group User', add_on_purchase_ids: query_add_on_purchase_ids },
query_fields
)
)
......@@ -122,7 +124,8 @@
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(
:addOnEligibleUsers,
{ add_on_type: :CODE_SUGGESTIONS, search: 'Group User', first: 1 },
{ add_on_type: :CODE_SUGGESTIONS, add_on_purchase_ids: query_add_on_purchase_ids, search: 'Group User',
first: 1 },
"pageInfo { endCursor } #{query_fields}"
)
)
......@@ -133,7 +136,8 @@
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(
:addOnEligibleUsers,
{ add_on_type: :CODE_SUGGESTIONS, search: 'Group User', after: end_cursor, first: 1 },
{ add_on_type: :CODE_SUGGESTIONS, add_on_purchase_ids: query_add_on_purchase_ids, search: 'Group User',
after: end_cursor, first: 1 },
query_fields
)
)
......@@ -169,7 +173,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(:addOnEligibleUsers, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:addOnEligibleUsers, add_on_params, query_fields)
)
end
......@@ -204,7 +208,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(:addOnEligibleUsers, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:addOnEligibleUsers, add_on_params, query_fields)
)
end
......@@ -245,7 +249,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(:add_on_eligible_users, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:add_on_eligible_users, add_on_params, query_fields)
)
end
......@@ -279,7 +283,7 @@
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(:add_on_eligible_users, { add_on_type: :CODE_SUGGESTIONS }, query_fields)
query_graphql_field(:add_on_eligible_users, add_on_params, query_fields)
)
end
......@@ -303,6 +307,42 @@
expect(graphql_data_at(:namespace, :add_on_eligible_users, :nodes, :add_on_assignments, :nodes).count).to eq(4)
end
end
context 'when there are filter args' do
let(:ineligible_user) { create(:user, name: 'Ineligible User') }
let(:query) do
graphql_query_for(
:namespace, { full_path: add_on_purchase.namespace.full_path },
query_graphql_field(
:addOnEligibleUsers,
{ add_on_type: :CODE_SUGGESTIONS, add_on_purchase_ids: query_add_on_purchase_ids,
filterByAssignedSeat: 'true' },
query_fields
)
)
end
it 'returns the add on eligible users and their assignments, filtered by assigned seat' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(:namespace, :add_on_eligible_users, :nodes))
.to eq([
{
'id' => global_id_of(developer).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
},
{
'id' => global_id_of(guest).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
},
{
'id' => global_id_of(current_user).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
}
])
end
end
end
def expected_add_on_purchase_data(expected_add_on_purchase)
......
......@@ -23,10 +23,12 @@
])
end
let(:add_on_params) { { addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids } }
let(:query) do
graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS },
add_on_params,
query_fields
)
end
......@@ -95,7 +97,7 @@
let(:query) do
graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS, search: 'Group User' },
{ addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids, search: 'Group User' },
query_fields
)
end
......@@ -119,7 +121,7 @@
it 'returns empty records if search term does not match any users' do
query_without_results = graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS, search: 'Nonexistent User' },
{ addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids, search: 'Nonexistent User' },
query_fields
)
......@@ -134,7 +136,8 @@
let(:first_page_query) do
graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS, search: 'Group User', first: 1 },
{ addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids, search: 'Group User',
first: 1 },
"pageInfo { endCursor } #{query_fields}"
)
end
......@@ -142,7 +145,8 @@
let(:second_page_query) do
graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS, search: 'Group User', after: end_cursor, first: 1 },
{ addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids, search: 'Group User',
after: end_cursor, first: 1 },
query_fields
)
end
......@@ -221,6 +225,36 @@
expect(graphql_data_at(:self_managed_add_on_eligible_users, :nodes, :add_on_assignments, :nodes).count).to eq(4)
end
end
context 'when there are filter options' do
let(:query) do
graphql_query_for(
:selfManagedAddOnEligibleUsers,
{ addOnType: :CODE_SUGGESTIONS, addOnPurchaseIds: query_add_on_purchase_ids, filterByAssignedSeat: 'true' },
query_fields
)
end
it 'returns the add on eligible users and their assignments, filtered by assigned seat' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(:self_managed_add_on_eligible_users, :nodes))
.to match_array([
{
'id' => global_id_of(active_user).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
},
{
'id' => global_id_of(guest_user).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
},
{
'id' => global_id_of(current_user).to_s,
'addOnAssignments' => { 'nodes' => [expected_add_on_purchase_data(add_on_purchase)] }
}
])
end
end
end
def expected_add_on_purchase_data(expected_add_on_purchase)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment