Commit 21e5c8d0 authored by James Lopez's avatar James Lopez

Merge branch '325541_subscription_details_view_graphql' into 'master'

Introduce Cloud License related GraphQL types

See merge request gitlab-org/gitlab!57382
parents 353b57c4 b4d7c836
......@@ -63,6 +63,12 @@ Returns [`ContainerRepositoryDetails`](#containerrepositorydetails).
| ---- | ---- | ----------- |
| `id` | [`ContainerRepositoryID!`](#containerrepositoryid) | The global ID of the container repository. |
### `currentLicense`
Fields related to the current license.
Returns [`CurrentLicense`](#currentlicense).
### `currentUser`
Get information about current user.
......@@ -181,6 +187,21 @@ Returns [`Iteration`](#iteration).
| ---- | ---- | ----------- |
| `id` | [`IterationID!`](#iterationid) | Find an iteration by its ID. |
### `licenseHistoryEntries`
Fields related to entries in the license history.
Returns [`LicenseHistoryEntryConnection`](#licensehistoryentryconnection).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
### `metadata`
Metadata about GitLab.
......@@ -1872,6 +1893,27 @@ Autogenerated return type of CreateTestCase.
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `testCase` | [`Issue`](#issue) | The test case created. |
### `CurrentLicense`
Represents the current license.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `activatedAt` | [`Date`](#date) | Date when the license was activated. |
| `billableUsersCount` | [`Int`](#int) | Number of billable users on the system. |
| `company` | [`String`](#string) | Company of the licensee. |
| `email` | [`String`](#string) | Email of the licensee. |
| `expiresAt` | [`Date`](#date) | Date when the license expires. |
| `id` | [`ID!`](#id) | ID of the license. |
| `lastSync` | [`Time`](#time) | Date when the license was last synced. |
| `maximumUserCount` | [`Int`](#int) | Highest number of billable users on the system during the term of the current license. |
| `name` | [`String`](#string) | Name of the licensee. |
| `plan` | [`String!`](#string) | Name of the subscription plan. |
| `startsAt` | [`Date`](#date) | Date when the license started. |
| `type` | [`String!`](#string) | Type of the license. |
| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. |
| `usersOverLicenseCount` | [`Int`](#int) | Number of users over the paid users in the license. |
### `CustomEmoji`
A custom emoji uploaded by user.
......@@ -3874,6 +3916,42 @@ An edge in a connection.
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`Label`](#label) | The item at the end of the edge. |
### `LicenseHistoryEntry`
Represents an entry from the Cloud License history.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `activatedAt` | [`Date`](#date) | Date when the license was activated. |
| `company` | [`String`](#string) | Company of the licensee. |
| `email` | [`String`](#string) | Email of the licensee. |
| `expiresAt` | [`Date`](#date) | Date when the license expires. |
| `id` | [`ID!`](#id) | ID of the license. |
| `name` | [`String`](#string) | Name of the licensee. |
| `plan` | [`String!`](#string) | Name of the subscription plan. |
| `startsAt` | [`Date`](#date) | Date when the license started. |
| `type` | [`String!`](#string) | Type of the license. |
| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. |
### `LicenseHistoryEntryConnection`
The connection type for LicenseHistoryEntry.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `edges` | [`[LicenseHistoryEntryEdge]`](#licensehistoryentryedge) | A list of edges. |
| `nodes` | [`[LicenseHistoryEntry]`](#licensehistoryentry) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
### `LicenseHistoryEntryEdge`
An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`LicenseHistoryEntry`](#licensehistoryentry) | The item at the end of the edge. |
### `MarkAsSpamSnippetPayload`
Autogenerated return type of MarkAsSpamSnippet.
......
......@@ -61,6 +61,16 @@ module EE
null: true,
description: 'Get configured DevOps adoption segments on the instance.',
resolver: ::Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver
field :current_license, ::Types::Admin::CloudLicenses::CurrentLicenseType,
null: true,
resolver: ::Resolvers::Admin::CloudLicenses::CurrentLicenseResolver,
description: 'Fields related to the current license.'
field :license_history_entries, ::Types::Admin::CloudLicenses::LicenseHistoryEntryType.connection_type,
null: true,
resolver: ::Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver,
description: 'Fields related to entries in the license history.'
end
def vulnerability(id:)
......
# frozen_string_literal: true
module Resolvers
module Admin
module CloudLicenses
class CurrentLicenseResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include ::Admin::LicenseRequest
type ::Types::Admin::CloudLicenses::CurrentLicenseType, null: true
def resolve
return unless application_settings.cloud_license_enabled?
authorize!
license
end
private
def application_settings
Gitlab::CurrentSettings.current_application_settings
end
def authorize!
Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error!
end
end
end
end
end
# frozen_string_literal: true
module Resolvers
module Admin
module CloudLicenses
class LicenseHistoryEntriesResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type [::Types::Admin::CloudLicenses::LicenseHistoryEntryType], null: true
def resolve
return unless application_settings.cloud_license_enabled?
authorize!
License.history
end
private
def application_settings
Gitlab::CurrentSettings.current_application_settings
end
def authorize!
Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error!
end
end
end
end
end
# frozen_string_literal: true
module Types
module Admin
module CloudLicenses
# rubocop: disable Graphql/AuthorizeTypes
class CurrentLicenseType < BaseObject
include ::Types::Admin::CloudLicenses::LicenseType
graphql_name 'CurrentLicense'
description 'Represents the current license'
field :last_sync, ::Types::TimeType, null: true,
description: 'Date when the license was last synced.',
method: :last_synced_at
field :billable_users_count, GraphQL::INT_TYPE, null: true,
description: 'Number of billable users on the system.',
method: :daily_billable_users_count
field :maximum_user_count, GraphQL::INT_TYPE, null: true,
description: 'Highest number of billable users on the system during the term of the current license.',
method: :maximum_user_count
field :users_over_license_count, GraphQL::INT_TYPE, null: true,
description: 'Number of users over the paid users in the license.'
def users_over_license_count
return 0 if object.trial?
[object.overage_with_historical_max, 0].max
end
end
end
end
end
# frozen_string_literal: true
module Types
module Admin
module CloudLicenses
# rubocop: disable Graphql/AuthorizeTypes
class LicenseHistoryEntryType < BaseObject
include ::Types::Admin::CloudLicenses::LicenseType
graphql_name 'LicenseHistoryEntry'
description 'Represents an entry from the Cloud License history'
end
end
end
end
# frozen_string_literal: true
module Types
module Admin
module CloudLicenses
module LicenseType
extend ActiveSupport::Concern
included do
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the license.',
method: :license_id
field :type, GraphQL::STRING_TYPE, null: false,
description: 'Type of the license.',
method: :license_type
field :plan, GraphQL::STRING_TYPE, null: false,
description: 'Name of the subscription plan.'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the licensee.',
method: :licensee_name
field :email, GraphQL::STRING_TYPE, null: true,
description: 'Email of the licensee.',
method: :licensee_email
field :company, GraphQL::STRING_TYPE, null: true,
description: 'Company of the licensee.',
method: :licensee_company
field :starts_at, ::Types::DateType, null: true,
description: 'Date when the license started.'
field :expires_at, ::Types::DateType, null: true,
description: 'Date when the license expires.'
field :activated_at, ::Types::DateType, null: true,
description: 'Date when the license was activated.',
method: :created_at
field :users_in_license_count, GraphQL::INT_TYPE, null: true,
description: 'Number of paid users in the license.',
method: :restricted_user_count
end
end
end
end
end
......@@ -8,6 +8,7 @@ class License < ApplicationRecord
PREMIUM_PLAN = 'premium'
ULTIMATE_PLAN = 'ultimate'
CLOUD_LICENSE_TYPE = 'cloud'
LEGACY_LICENSE_TYPE = 'legacy'
ALLOWED_PERCENTAGE_OF_USERS_OVERAGE = (10 / 100.0)
EE_ALL_PLANS = [STARTER_PLAN, PREMIUM_PLAN, ULTIMATE_PLAN].freeze
......@@ -237,6 +238,8 @@ class License < ApplicationRecord
{ range: (1000..nil), percentage: true, value: 5 }
].freeze
LICENSEE_ATTRIBUTES = %w[Name Email Company].freeze
validate :valid_license
validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup?
validate :check_trueup, unless: :persisted?, if: :validate_with_trueup?
......@@ -550,6 +553,10 @@ class License < ApplicationRecord
license&.type == CLOUD_LICENSE_TYPE
end
def license_type
cloud? ? CLOUD_LICENSE_TYPE : LEGACY_LICENSE_TYPE
end
def auto_renew
false
end
......@@ -576,6 +583,12 @@ class License < ApplicationRecord
restricted_user_count - daily_billable_users_count
end
LICENSEE_ATTRIBUTES.each do |attribute|
define_method "licensee_#{attribute.downcase}" do
licensee[attribute]
end
end
private
def restricted_attr(name, default = nil)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Admin::CloudLicenses::CurrentLicenseResolver do
include GraphqlHelpers
specify do
expect(described_class).to have_nullable_graphql_type(::Types::Admin::CloudLicenses::CurrentLicenseType)
end
describe '#resolve' do
subject(:result) { resolve_current_license }
let_it_be(:admin) { create(:admin) }
let_it_be(:license) { create_current_license }
def resolve_current_license(current_user: admin)
resolve(described_class, ctx: { current_user: current_user })
end
before do
stub_application_setting(cloud_license_enabled: true)
end
context 'when application setting for cloud license is disabled', :enable_admin_mode do
it 'returns nil' do
stub_application_setting(cloud_license_enabled: false)
expect(result).to be_nil
end
end
context 'when current user is unauthorized' do
it 'raises error' do
unauthorized_user = create(:user)
expect do
resolve_current_license(current_user: unauthorized_user)
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when there is no current license', :enable_admin_mode do
it 'returns nil' do
License.delete_all # delete existing license
expect(result).to be_nil
end
end
it 'returns the current license', :enable_admin_mode do
expect(result).to eq(license)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver do
include GraphqlHelpers
describe '#resolve' do
subject(:result) { resolve_license_history_entries }
let_it_be(:admin) { create(:admin) }
def create_license(data: {}, license_options: { created_at: Time.current })
gl_license = create(:gitlab_license, data)
create(:license, license_options.merge(data: gl_license.export))
end
def resolve_license_history_entries(current_user: admin)
resolve(described_class, ctx: { current_user: current_user })
end
before do
stub_application_setting(cloud_license_enabled: true)
end
context 'when application setting for cloud license is disabled', :enable_admin_mode do
it 'returns nil' do
stub_application_setting(cloud_license_enabled: false)
expect(result).to be_nil
end
end
context 'when current user is unauthorized' do
it 'raises error' do
unauthorized_user = create(:user)
expect do
resolve_license_history_entries(current_user: unauthorized_user)
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when no licenses exist' do
it 'returns an empty array', :enable_admin_mode do
License.delete_all # delete license created with ee/spec/support/test_license.rb
expect(result).to eq([])
end
end
it 'returns the license history entries', :enable_admin_mode do
today = Date.current
type = License::CLOUD_LICENSE_TYPE
past_license = create_license(
data: { starts_at: today - 1.month, expires_at: today + 11.months },
license_options: { created_at: Time.current - 1.month }
)
expired_license = create_license(data: { starts_at: today - 1.year, expires_at: today - 1.month })
another_license = create_license(data: { starts_at: today - 1.month, expires_at: today + 1.year })
future_license = create_license(data: { starts_at: today + 1.month, expires_at: today + 13.months, type: type })
current_license = create_license(data: { starts_at: today - 15.days, expires_at: today + 11.months, type: type })
expect(result).to eq(
[
future_license,
current_license,
another_license,
past_license,
expired_license,
License.first # created with ee/spec/support/test_license.rb
]
)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CurrentLicense'], :enable_admin_mode do
let_it_be(:admin) { create(:admin) }
let_it_be(:licensee) do
{
'Name' => 'User Example',
'Email' => 'user@example.com',
'Company' => 'Example Inc.'
}
end
let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) }
let(:fields) do
%w[last_sync billable_users_count maximum_user_count users_over_license_count]
end
def query(field_name)
%(
{
currentLicense {
#{field_name}
}
}
)
end
def query_field(field_name)
GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json
end
before do
stub_application_setting(cloud_license_enabled: true)
end
it { expect(described_class.graphql_name).to eq('CurrentLicense') }
it { expect(described_class).to include_graphql_fields(*fields) }
include_examples 'license type fields', %w[data currentLicense]
describe "#users_over_license_count" do
context 'when license is for a trial' do
it 'returns 0' do
create_current_license(licensee: licensee, restrictions: { trial: true })
result_as_json = query_field('usersOverLicenseCount')
expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(0)
end
end
it 'returns the number of users over the paid users in the license' do
create(:historical_data, active_user_count: 15)
create_current_license(licensee: licensee, restrictions: { active_user_count: 10 })
result_as_json = query_field('usersOverLicenseCount')
expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(5)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['LicenseHistoryEntry'], :enable_admin_mode do
let_it_be(:admin) { create(:admin) }
let_it_be(:licensee) do
{
'Name' => 'User Example',
'Email' => 'user@example.com',
'Company' => 'Example Inc.'
}
end
let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) }
def query(field_name)
%(
{
licenseHistoryEntries {
nodes {
#{field_name}
}
}
}
)
end
def query_field(field_name)
GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json
end
before do
stub_application_setting(cloud_license_enabled: true)
end
it { expect(described_class.graphql_name).to eq('LicenseHistoryEntry') }
include_examples 'license type fields', ['data', 'licenseHistoryEntries', 'nodes', -1]
end
......@@ -10,7 +10,9 @@ RSpec.describe GitlabSchema.types['Query'] do
:vulnerabilities,
:vulnerability,
:instance_security_dashboard,
:vulnerabilities_count_by_day_and_severity
:vulnerabilities_count_by_day_and_severity,
:current_license,
:license_history_entries
).at_least
end
end
......@@ -1411,6 +1411,20 @@ RSpec.describe License do
end
end
describe '#license_type' do
subject { license.license_type }
context 'when the license is not a cloud license' do
it { is_expected.to eq(described_class::LEGACY_LICENSE_TYPE) }
end
context 'when the license is a cloud license' do
let(:gl_license) { build(:gitlab_license, type: described_class::CLOUD_LICENSE_TYPE) }
it { is_expected.to eq(described_class::CLOUD_LICENSE_TYPE) }
end
end
describe '#auto_renew' do
it 'is false' do
expect(license.auto_renew).to be false
......@@ -1485,4 +1499,28 @@ RSpec.describe License do
it { is_expected.to eq(result) }
end
end
describe '#licensee_name' do
subject { license.licensee_name }
let(:gl_license) { build(:gitlab_license, licensee: { 'Name' => 'User Example' }) }
it { is_expected.to eq('User Example') }
end
describe '#licensee_email' do
subject { license.licensee_email }
let(:gl_license) { build(:gitlab_license, licensee: { 'Email' => 'user@example.com' }) }
it { is_expected.to eq('user@example.com') }
end
describe '#licensee_company' do
subject { license.licensee_company }
let(:gl_license) { build(:gitlab_license, licensee: { 'Company' => 'Example Inc.' }) }
it { is_expected.to eq('Example Inc.') }
end
end
# frozen_string_literal: true
RSpec.shared_examples_for 'license type fields' do |keys|
context 'with license type fields' do
let(:license_fields) do
%w[id type plan name email company starts_at expires_at activated_at users_in_license_count]
end
it { expect(described_class).to include_graphql_fields(*license_fields) }
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