Skip to content
Snippets Groups Projects
Commit 1a714080 authored by Nikola Milojevic's avatar Nikola Milojevic :large_blue_circle:
Browse files

Introduce AvailableServices singleton for accessing data

- Parse bundle_with json from CloudConnector::Access record
parent a3b1b45f
No related branches found
No related tags found
1 merge request!148513Introduce AvailableServices singleton for accessing data
......@@ -5,5 +5,11 @@ class Access < ApplicationRecord
self.table_name = 'cloud_connector_access'
validates :data, json_schema: { filename: "cloud_connector_access" }
validates :data, presence: true
after_save :clear_available_services_cache!
def clear_available_services_cache!
Rails.cache.delete(::CloudConnector::AvailableServices::CLOUD_CONNECTOR_SERVICES_KEY)
end
end
end
......@@ -4,27 +4,39 @@ module GitlabSubscriptions
class UserAddOnAssignment < ApplicationRecord
include EachBatch
USER_ADD_ON_ASSIGNMENT_CACHE_KEY = 'addon-assignments:user-%{user_id}'
belongs_to :user, inverse_of: :assigned_add_ons
belongs_to :add_on_purchase, class_name: 'GitlabSubscriptions::AddOnPurchase', inverse_of: :assigned_users
validates :user, :add_on_purchase, presence: true
validates :add_on_purchase_id, uniqueness: { scope: :user_id }
after_save :clear_user_add_on_assigment_cache!
scope :by_user, ->(user) { where(user: user) }
scope :for_user_ids, ->(user_ids) { where(user_id: user_ids) }
scope :with_namespaces, -> { includes(add_on_purchase: :namespace) }
scope :for_active_add_on_purchases, ->(add_on_purchases) do
joins(:add_on_purchase).merge(add_on_purchases.active)
end
scope :for_active_gitlab_duo_pro_purchase, -> do
joins(:add_on_purchase).merge(::GitlabSubscriptions::AddOnPurchase.active.for_gitlab_duo_pro)
for_active_add_on_purchases(::GitlabSubscriptions::AddOnPurchase.for_gitlab_duo_pro)
end
scope :for_active_add_on_purchase_ids, ->(add_on_purchase_ids) do
joins(:add_on_purchase)
.merge(::GitlabSubscriptions::AddOnPurchase.where(id: add_on_purchase_ids).active)
for_active_add_on_purchases(::GitlabSubscriptions::AddOnPurchase.where(id: add_on_purchase_ids))
end
def self.pluck_user_ids
pluck(:user_id)
end
def clear_user_add_on_assigment_cache!
cache_key = format(USER_ADD_ON_ASSIGNMENT_CACHE_KEY, user_id: user.id)
Rails.cache.delete(cache_key)
end
end
end
# frozen_string_literal: true
# Presents a service enabled through Cloud Connector
module CloudConnector
class AvailableServiceData
include ::Gitlab::Utils::StrongMemoize
attr_accessor :name, :cut_off_date
def initialize(name, cut_off_date, add_on_names)
@name = name
@cut_off_date = cut_off_date
@add_on_names = add_on_names
end
def free_access?
cut_off_date.nil? || cut_off_date&.future?
end
def allowed_for?(user)
cache_key = format(GitlabSubscriptions::UserAddOnAssignment::USER_ADD_ON_ASSIGNMENT_CACHE_KEY, user_id: user.id)
Rails.cache.fetch(cache_key) do
GitlabSubscriptions::UserAddOnAssignment.by_user(user)
.for_active_add_on_purchases(add_on_purchases).any?
end
end
def access_token
::CloudConnector::ServiceAccessToken.active.last&.token
end
private
def add_on_purchases
GitlabSubscriptions::AddOnPurchase.by_add_on_name(@add_on_names)
end
strong_memoize_attr :add_on_purchases
end
end
# frozen_string_literal: true
module CloudConnector
class AvailableServices
include Singleton
CLOUD_CONNECTOR_SERVICES_KEY = 'cloud-connector:services'
class << self
def find_by_name(name)
instance.available_services[name]
end
end
def available_services
Rails.cache.fetch(CLOUD_CONNECTOR_SERVICES_KEY) do
service_descriptors = access_record_data&.[]('available_services') || []
service_descriptors.map { |access_data| build_available_service_data(access_data) }.index_by(&:name)
end
end
private
def access_record_data
::CloudConnector::Access.last&.data
end
def parse_time(time)
Time.zone.parse(time).utc if time
end
def build_available_service_data(access_data)
::CloudConnector::AvailableServiceData.new(
access_data['name'].to_sym,
parse_time(access_data["serviceStartTime"]),
access_data["bundledWith"].to_a
)
end
end
end
......@@ -2,6 +2,21 @@
FactoryBot.define do
factory :cloud_connector_access, class: 'CloudConnector::Access' do
data { { available_services: [{ name: "code_suggestions", service_start_time: "2024-02-15T00:00:00Z" }] } }
data do
{
available_services: [
{
name: "code_suggestions",
service_start_time: "2024-02-15T00:00:00Z",
bundled_with: %w[duo_pro]
},
{
name: "duo_chat",
service_start_time: nil,
bundled_with: %w[duo_pro duo_extra]
}
]
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CloudConnector::AvailableServiceData, feature_category: :cloud_connector do
let_it_be(:cut_off_date) { 1.day.ago }
let_it_be(:purchased_add_ons) { %w[code_suggestions] }
describe '#free_access?' do
subject(:access_token) { described_class.new(:duo_chat, cut_off_date, nil).free_access? }
context 'when cut_off_date is in the past' do
let_it_be(:cut_off_date) { 1.day.ago }
it { is_expected.to be false }
end
context 'when cut_off_date is in the future' do
let_it_be(:cut_off_date) { 1.day.from_now }
it { is_expected.to be true }
end
end
describe '#allowed_for?', :redis do
let_it_be(:gitlab_add_on) { create(:gitlab_subscription_add_on) }
let_it_be(:user) { create(:user) }
let_it_be(:expired_gitlab_purchase) do
create(:gitlab_subscription_add_on_purchase, expires_on: 1.day.ago, add_on: gitlab_add_on)
end
let_it_be_with_reload(:active_gitlab_purchase) do
create(:gitlab_subscription_add_on_purchase, :self_managed, add_on: gitlab_add_on)
end
subject(:allowed_for?) { described_class.new(:duo_chat, cut_off_date, purchased_add_ons).allowed_for?(user) }
context 'when the user has an active assigned seat' do
before do
create(
:gitlab_subscription_user_add_on_assignment,
user: user,
add_on_purchase: active_gitlab_purchase
)
end
it { is_expected.to be true }
it 'caches the available services' do
expect(GitlabSubscriptions::UserAddOnAssignment)
.to receive_message_chain(:by_user, :for_active_add_on_purchases, :any?)
2.times do
allowed_for?
end
end
end
context 'when the user has an expired assigned duo pro seat' do
before do
create(
:gitlab_subscription_user_add_on_assignment,
user: user,
add_on_purchase: expired_gitlab_purchase
)
end
it { is_expected.to be false }
end
context 'when the user has no add on seat assignments' do
it { is_expected.to be false }
end
end
describe '#name' do
subject(:name) { described_class.new(:duo_chat, cut_off_date, purchased_add_ons).name }
it { is_expected.to eq(:duo_chat) }
end
describe '#access_token' do
subject(:access_token) { described_class.new(:duo_chat, nil, nil).access_token }
let_it_be(:older_active_token) { create(:service_access_token, :active) }
let_it_be(:newer_active_token) { create(:service_access_token, :active) }
let_it_be(:inactive_token) { create(:service_access_token, :expired) }
it { is_expected.to eq(newer_active_token.token) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CloudConnector::AvailableServices, feature_category: :cloud_connector do
let_it_be(:cs_cut_off_date) { Time.zone.parse("2024-02-15 00:00:00 UTC").utc }
let_it_be(:cs_bundled_with) { %w[duo_pro] }
let_it_be(:duo_chat_bundled_with) { %w[duo_pro duo_extra] }
let_it_be(:data) do
{
available_services: [
{
"name" => "code_suggestions",
"serviceStartTime" => cs_cut_off_date.to_s,
"bundledWith" => cs_bundled_with
},
{
"name" => "duo_chat",
"serviceStartTime" => nil,
"bundledWith" => duo_chat_bundled_with
}
]
}
end
let_it_be(:cloud_connector_access) { create(:cloud_connector_access, data: data) }
let_it_be(:available_service_data_class) { CloudConnector::AvailableServiceData }
describe '.find_by_name' do
it 'reads available service' do
service = described_class.find_by_name(:duo_chat)
expect(service.name).to eq(:duo_chat)
expect(service).is_a?(available_service_data_class)
end
end
describe '#available_services', :redis do
let_it_be(:arguments_map) do
{
code_suggestions: [cs_cut_off_date, cs_bundled_with],
duo_chat: [nil, duo_chat_bundled_with]
}
end
subject(:available_services) { described_class.instance.available_services }
it 'creates AvailableServiceData with correct params' do
arguments_map.each do |name, args|
expect(available_service_data_class).to receive(:new).with(name, *args).and_call_original
end
available_services
end
it 'caches the available services' do
arguments_map.each do |name, args|
expect(available_service_data_class).to receive(:new).with(name, *args).and_call_original.once
end
2.times do
available_services
end
end
it 'returns a hash containing all available services', :aggregate_failures do
expect(available_services.keys).to match_array(arguments_map.keys)
expect(available_services.values).to all(be_instance_of(available_service_data_class))
end
end
end
......@@ -10,4 +10,31 @@
it { is_expected.to validate_presence_of(:data) }
end
describe 'callbacks' do
describe 'after_save' do
subject(:access) { build(:cloud_connector_access) }
it 'calls #clear_available_services_cache!' do
is_expected.to receive(:clear_available_services_cache!)
access.save!
end
end
end
describe '#clear_available_services_cache!', :use_clean_rails_memory_store_caching do
let(:cache_key) { CloudConnector::AvailableServices::CLOUD_CONNECTOR_SERVICES_KEY }
before do
Rails.cache.write(cache_key, double)
end
it 'clears cache' do
access = create(:cloud_connector_access)
access.clear_available_services_cache!
expect(Rails.cache.read(cache_key)).to be_nil
end
end
end
......@@ -74,6 +74,37 @@
end
end
describe '.for_active_add_on_purchases' do
context 'when the assignment is for an active addon purchase' do
it 'is included in the scope' do
purchase = create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro)
assignment = create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: purchase)
purchases = ::GitlabSubscriptions::AddOnPurchase.where(id: purchase.id)
expect(described_class.for_active_add_on_purchases(purchases)).to eq [assignment]
end
end
context 'when the assignment is for an expired addon purchase' do
it 'is not included in the scope' do
purchase = create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro, expires_on: 1.week.ago)
create(:gitlab_subscription_user_add_on_assignment, add_on_purchase: purchase)
purchases = ::GitlabSubscriptions::AddOnPurchase.where(id: purchase.id)
expect(described_class.for_active_add_on_purchases(purchases)).to be_empty
end
end
context 'when there are no assignments for an active gitlab duo pro purchase' do
it 'returns an empty relation' do
purchase = create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro)
purchases = ::GitlabSubscriptions::AddOnPurchase.where(id: purchase.id)
expect(described_class.for_active_add_on_purchases(purchases)).to be_empty
end
end
end
describe '.for_active_gitlab_duo_pro_purchase' do
context 'when the assignment is for an active gitlab duo pro purchase' do
it 'is included in the scope' do
......@@ -156,6 +187,19 @@
end
end
describe 'callbacks' do
describe 'after_save' do
let(:user) { create(:user) }
subject(:assigment) { build(:gitlab_subscription_user_add_on_assignment, user: user) }
it 'calls #clear_user_add_on_assigment_cache!' do
is_expected.to receive(:clear_user_add_on_assigment_cache!)
assigment.save!
end
end
end
describe '.pluck_user_ids' do
it 'plucks the user ids' do
user = create(:user)
......@@ -164,4 +208,21 @@
expect(described_class.where(id: assignment).pluck_user_ids).to match_array([user.id])
end
end
describe '#clear_user_add_on_assigment_cache!', :use_clean_rails_memory_store_caching do
let(:user) { create(:user) }
let(:cache_key) { format(described_class::USER_ADD_ON_ASSIGNMENT_CACHE_KEY, user_id: user.id) }
before do
Rails.cache.write(cache_key, double)
end
it 'clears cache' do
assignment = create(:gitlab_subscription_user_add_on_assignment, user: user)
assignment.clear_user_add_on_assigment_cache!
expect(Rails.cache.read(cache_key)).to be_nil
end
end
end
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