Commit 3706a222 authored by Josianne Hyson's avatar Josianne Hyson

Add client endpoint to filter eligible namespaces

We want to fetch the eligibility status of a namespace to purchase a
subscription from the customer's app. This way, we do not have to
replicate the eligibility logic between gitlab and the customer's app.

Add an endpoint to the client that can be used to fetch the eligibility
status.

Issue: https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/2344
parent c0753f25
# frozen_string_literal: true
module GitlabSubscriptions
class FilterPurchaseEligibleNamespacesService
include ::Gitlab::Utils::StrongMemoize
def initialize(user:, namespaces:)
@user = user
@namespaces = namespaces
end
def execute
return success([]) if namespaces.empty?
return missing_user_error if user.nil?
if response[:success]
eligible_ids = response[:data].map { |data| data['id'] }.to_set
data = namespaces.filter { |namespace| eligible_ids.include?(namespace.id) }
success(data)
else
error('Failed to fetch namespaces', response.dig(:data, :errors))
end
end
private
attr_reader :user, :namespaces
def success(payload)
ServiceResponse.success(payload: payload)
end
def error(message, payload = nil)
ServiceResponse.error(message: message, payload: payload)
end
def missing_user_error
message = 'User cannot be nil'
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(message))
error(message)
end
def response
strong_memoize(:response) do
Gitlab::SubscriptionPortal::Client.filter_purchase_eligible_namespaces(user, namespaces)
end
end
end
end
......@@ -88,8 +88,7 @@ module Gitlab
response = execute_graphql_query({ query: query, variables: { tags: plan_tags } })[:data]
if response['errors'].present?
exception = SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, query: query, response: response)
track_error(query, response)
return error
end
......@@ -121,6 +120,39 @@ module Gitlab
end
end
def filter_purchase_eligible_namespaces(user, namespaces)
query = <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
namespace_data = namespaces.map do |namespace|
{
id: namespace.id,
parentId: namespace.parent_id,
plan: namespace.actual_plan_name,
trial: !!namespace.trial?
}
end
response = http_post(
"graphql",
admin_headers,
{ query: query, variables: { customerUid: user.id, namespaces: namespace_data } }
)[:data]
if response['errors'].blank?
{ success: true, data: response.dig('data', 'namespaceEligibility') }
else
track_error(query, response)
error(response['errors'])
end
end
private
def execute_graphql_query(params)
......@@ -137,6 +169,14 @@ module Gitlab
EE::SUBSCRIPTIONS_GRAPHQL_URL
end
def track_error(query, response)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot"),
query: query,
response: response
)
end
def error(errors = nil)
{
success: false,
......
......@@ -244,4 +244,100 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do
end
end
end
describe '#filter_purchase_eligible_namespaces' do
subject do
client.filter_purchase_eligible_namespaces(user, [user_namespace, group_namespace, subgroup])
end
let_it_be(:user) { create(:user) }
let_it_be(:user_namespace) { user.namespace }
let_it_be(:group_namespace) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group_namespace) }
let(:headers) do
{
"Accept" => "application/json",
"Content-Type" => "application/json",
"X-Admin-Email" => "gl_com_api@gitlab.com",
"X-Admin-Token" => "customer_admin_token"
}
end
let(:variables) do
{
customerUid: user.id,
namespaces: [
{ id: user_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: group_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: subgroup.id, parentId: group_namespace.id, plan: "default", trial: false }
]
}
end
let(:params) do
{
variables: variables,
query: <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
}
end
context 'when the response is successful' do
it 'returns the namespace data', :aggregate_failures do
response = {
data: {
'data' => {
'namespaceEligibility' => [{ 'id' => 1 }, { 'id' => 3 }]
}
}
}
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(success: true, data: [{ 'id' => 1 }, { 'id' => 3 }])
end
end
context 'when the response is unsuccessful' do
it 'returns the error message', :aggregate_failures do
response = {
data: {
"data" => {
"namespaceEligibility" => nil
},
"errors" => [
{
"message" => "You must be logged in to access this resource",
"locations" => [{ "line" => 2, "column" => 3 }],
"path" => ["namespaceEligibility"]
}
]
}
}
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(
a_kind_of(Gitlab::SubscriptionPortal::Client::ResponseError),
query: params[:query], response: response[:data])
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(
success: false,
errors: [{
"locations" => [{ "column" => 3, "line" => 2 }],
"message" => "You must be logged in to access this resource",
"path" => ["namespaceEligibility"]
}]
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::FilterPurchaseEligibleNamespacesService do
describe '#execute' do
let_it_be(:user) { build(:user) }
context 'when no namespaces are supplied' do
it 'returns an empty array', :aggregate_failures do
result = described_class.new(user: user, namespaces: []).execute
expect(result).to be_success
expect(result.payload).to eq []
end
end
context 'when no user is supplied' do
subject(:service) { described_class.new(user: nil, namespaces: [build(:namespace)]) }
it 'logs and returns an error message', :aggregate_failures do
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with an_instance_of(ArgumentError)
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'User cannot be nil'
expect(result.payload).to be_nil
end
end
context 'when the http request fails' do
subject(:service) { described_class.new(user: user, namespaces: [create(:namespace)]) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: false, data: { errors: 'error' }
)
end
it 'returns an error message', :aggregate_failures do
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'Failed to fetch namespaces'
expect(result.payload).to eq 'error'
end
end
context 'when all the namespaces are eligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }, { 'id' => namespace_2.id }]
)
end
it 'does not filter any namespaces', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq namespaces
end
end
context 'when the user has a namespace ineligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }]
)
end
it 'is filtered from the results', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq [namespace_1]
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment