Commit 3150ee76 authored by Pavel Shutsin's avatar Pavel Shutsin

Refactor user_type related scopes

Move all user_types to FOSS, update related
scopes to be more straightforward
parent 26fded71
# frozen_string_literal: true
module HasUserType
extend ActiveSupport::Concern
USER_TYPES = {
human: nil,
support_bot: 1,
alert_bot: 2,
visual_review_bot: 3,
service_user: 4,
ghost: 5,
project_bot: 6
}.with_indifferent_access.freeze
BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot].freeze
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
included do
scope :humans, -> { where(user_type: :human) }
scope :bots, -> { where(user_type: BOT_USER_TYPES) }
scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
enum user_type: USER_TYPES
def human?
super || user_type.nil?
end
end
def bot?
BOT_USER_TYPES.include?(user_type)
end
# The explicit check for project_bot will be removed with Bot Categorization
# Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
def internal?
ghost? || (bot? && !project_bot?)
end
end
...@@ -24,6 +24,7 @@ class User < ApplicationRecord ...@@ -24,6 +24,7 @@ class User < ApplicationRecord
include HasUniqueInternalUsers include HasUniqueInternalUsers
include IgnorableColumns include IgnorableColumns
include UpdateHighestRole include UpdateHighestRole
include HasUserType
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
...@@ -64,10 +65,10 @@ class User < ApplicationRecord ...@@ -64,10 +65,10 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180 MINIMUM_INACTIVE_DAYS = 180
enum user_type: ::UserTypeEnums.types
ignore_column :bot_type, remove_with: '12.11', remove_after: '2020-04-22' ignore_column :bot_type, remove_with: '12.11', remove_after: '2020-04-22'
ignore_column :ghost, remove_with: '13.2', remove_after: '2020-06-22'
# Override Devise::Models::Trackable#update_tracked_fields! # Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour # to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
...@@ -321,33 +322,26 @@ class User < ApplicationRecord ...@@ -321,33 +322,26 @@ class User < ApplicationRecord
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) } scope :external, -> { where(external: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, -> { with_state(:active).non_internal } scope :active, -> { with_state(:active).non_internal }
scope :active_without_ghosts, -> { with_state(:active).without_ghosts } scope :active_without_ghosts, -> { with_state(:active).without_ghosts }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
scope :non_internal, -> { without_ghosts.with_project_bots }
scope :deactivated, -> { with_state(:deactivated).non_internal } scope :deactivated, -> { with_state(:deactivated).non_internal }
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) } scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) }
scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) } scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
scope :with_emails, -> { preload(:emails) } scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) } scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) } scope :with_public_profile, -> { where(private_profile: false) }
scope :bots, -> { where(user_type: UserTypeEnums.bots.values) }
scope :bots_without_project_bot, -> { bots.where.not(user_type: UserTypeEnums.bots[:project_bot]) }
scope :with_project_bots, -> { humans.or(where.not(user_type: UserTypeEnums.bots.except(:project_bot).values)) }
scope :humans, -> { where(user_type: nil) }
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
where('EXISTS (?)', where('EXISTS (?)',
::PersonalAccessToken ::PersonalAccessToken
.where('personal_access_tokens.user_id = users.id') .where('personal_access_tokens.user_id = users.id')
.expiring_and_not_notified(at).select(1)) .expiring_and_not_notified(at).select(1))
end end
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
def active_for_authentication? def active_for_authentication?
super && can?(:log_in) super && can?(:log_in)
...@@ -659,16 +653,6 @@ class User < ApplicationRecord ...@@ -659,16 +653,6 @@ class User < ApplicationRecord
username username
end end
def bot?
UserTypeEnums.bots.has_key?(user_type)
end
# The explicit check for project_bot will be removed with Bot Categorization
# Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
def internal?
ghost? || (bot? && !project_bot?)
end
def to_param def to_param
username username
end end
......
# frozen_string_literal: true
module UserTypeEnums
def self.types
@types ||= bots.merge(human: nil, ghost: 5)
end
def self.bots
@bots ||= { alert_bot: 2, project_bot: 6 }.with_indifferent_access
end
end
UserTypeEnums.prepend_if_ee('EE::UserTypeEnums')
# frozen_string_literal: true
module EE
module UserTypeEnums
extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :types
def types
@types ||= super.merge(service_user: 4)
end
override :bots
def bots
@bots ||= super.merge(support_bot: 1, visual_review_bot: 3)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe UserTypeEnums do
it 'has no type conflicts between CE and EE', :aggregate_failures do
described_class.public_methods(false).each do |method_name|
method = described_class.method(method_name)
ee_result = method.call
ce_result = method.super_method.call
failure_message = "expected #{method} to have no value conflicts, but it has.\n
Please make sure you are not overriding values.\n
Actual values: EE #{ee_result}, CE #{ce_result}"
expect(ee_result).to include(ce_result), failure_message
expect(ee_result.values).to match_array(ee_result.values.uniq)
end
end
end
...@@ -87,31 +87,6 @@ describe User do ...@@ -87,31 +87,6 @@ describe User do
end end
end end
describe '.active_without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
let_it_be(:user3) { create(:user, :ghost) }
let_it_be(:user4) { create(:user, user_type: :support_bot) }
let_it_be(:user5) { create(:user, state: 'blocked', user_type: :support_bot) }
it 'returns all active users including active bots but ghost users' do
expect(described_class.active_without_ghosts).to match_array([user1, user4])
end
end
describe '.non_internal' do
let!(:user) { create(:user) }
let!(:service_user) { create(:user, user_type: :service_user) }
let!(:ghost) { described_class.ghost }
let!(:alert_bot) { described_class.alert_bot }
let!(:non_internal) { [user, service_user] }
it 'returns users without ghosts and bots' do
expect(described_class.non_internal).to match_array(non_internal)
expect(non_internal.all?(&:internal?)).to eq(false)
end
end
describe '.managed_by' do describe '.managed_by' do
let!(:group) { create(:group_with_managed_accounts) } let!(:group) { create(:group_with_managed_accounts) }
let!(:managed_users) { create_list(:user, 2, managing_group: group) } let!(:managed_users) { create_list(:user, 2, managing_group: group) }
...@@ -622,27 +597,19 @@ describe User do ...@@ -622,27 +597,19 @@ describe User do
context 'when user is internal' do context 'when user is internal' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:bot_user_type) do where(:internal_user_type) do
UserTypeEnums.bots.keys described_class::INTERNAL_USER_TYPES
end end
with_them do with_them do
context 'when user is a bot' do context 'when user has internal user type' do
let(:user) { create(:user, user_type: bot_user_type) } let(:user) { create(:user, user_type: internal_user_type) }
it 'returns false' do it 'returns false' do
expect(user.using_license_seat?).to eq false expect(user.using_license_seat?).to eq false
end end
end end
end end
context 'when user is a ghost' do
let(:user) { create(:user, :ghost) }
it 'returns false' do
expect(user.using_license_seat?).to eq false
end
end
end end
context 'when user is not internal' do context 'when user is not internal' do
...@@ -1140,7 +1107,7 @@ describe User do ...@@ -1140,7 +1107,7 @@ describe User do
end end
context 'when user is ghost' do context 'when user is ghost' do
let(:user) { build(:user, email: 'test@gitlab.com', ghost: true) } let(:user) { build(:user, :ghost, email: 'test@gitlab.com') }
it { is_expected.to be false } it { is_expected.to be false }
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe User do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES)
.to include(*%i[human ghost alert_bot project_bot support_bot service_user visual_review_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
end
describe 'scopes & predicates' do
User::USER_TYPES.keys.each do |type|
let_it_be(type) { create(:user, username: type, user_type: type) }
end
let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
let(:non_internal) { User::NON_INTERNAL_USER_TYPES.map { |type| public_send(type) } }
let(:everyone) { User::USER_TYPES.keys.map { |type| public_send(type) } }
describe '.humans' do
it 'includes humans only' do
expect(described_class.humans).to match_array([human])
end
end
describe '.bots' do
it 'includes all bots' do
expect(described_class.bots).to match_array(bots)
end
end
describe '.bots_without_project_bot' do
it 'includes all bots except project_bot' do
expect(described_class.bots_without_project_bot).to match_array(bots - [project_bot])
end
end
describe '.non_internal' do
it 'includes all non_internal users' do
expect(described_class.non_internal).to match_array(non_internal)
end
end
describe '.without_ghosts' do
it 'includes everyone except ghosts' do
expect(described_class.without_ghosts).to match_array(everyone - [ghost])
end
end
describe '#bot?' do
it 'is true for all bot user types and false for others' do
expect(bots).to all(be_bot)
(everyone - bots).each do |user|
expect(user).not_to be_bot
end
end
end
describe '#human?' do
it 'is true for humans only' do
expect(human).to be_human
expect(alert_bot).not_to be_human
expect(User.new).to be_human
end
end
describe '#internal?' do
it 'is true for all internal user types and false for others' do
expect(everyone - non_internal).to all(be_internal)
non_internal.each do |user|
expect(user).not_to be_internal
end
end
end
end
end
...@@ -4354,31 +4354,15 @@ describe User, :do_not_mock_admin_mode do ...@@ -4354,31 +4354,15 @@ describe User, :do_not_mock_admin_mode do
end end
end end
describe '.non_internal' do describe '.active_without_ghosts' do
let!(:user) { create(:user) } let_it_be(:user1) { create(:user, :external) }
let!(:ghost) { described_class.ghost } let_it_be(:user2) { create(:user, state: 'blocked') }
let!(:alert_bot) { described_class.alert_bot } let_it_be(:user3) { create(:user, :ghost) }
let!(:project_bot) { create(:user, :project_bot) } let_it_be(:user4) { create(:user, user_type: :support_bot) }
let(:non_internal) { [user, project_bot] } let_it_be(:user5) { create(:user, state: 'blocked', user_type: :support_bot) }
it 'returns non internal users' do
expect(described_class.non_internal).to eq(non_internal)
expect(non_internal.all?(&:internal?)).to eq(false)
end
end
describe '#bot?' do
let!(:user) { create(:user) }
let!(:ghost) { described_class.ghost }
let!(:alert_bot) { described_class.alert_bot }
let!(:project_bot) { create(:user, :project_bot) }
it 'marks bot users' do
expect(user).not_to be_bot
expect(ghost).not_to be_bot
expect(alert_bot).to be_bot it 'returns all active users including active bots but ghost users' do
expect(project_bot).to be_bot expect(described_class.active_without_ghosts).to match_array([user1, user4])
end end
end end
...@@ -4416,19 +4400,6 @@ describe User, :do_not_mock_admin_mode do ...@@ -4416,19 +4400,6 @@ describe User, :do_not_mock_admin_mode do
end end
end end
describe 'bots & humans' do
it 'returns corresponding users' do
human = create(:user)
bot = create(:user, :bot)
project_bot = create(:user, :project_bot)
expect(described_class.humans).to match_array([human])
expect(described_class.bots).to match_array([bot, project_bot])
expect(described_class.bots_without_project_bot).to match_array([bot])
expect(described_class.with_project_bots).to match_array([human, project_bot])
end
end
describe '#hook_attrs' do describe '#hook_attrs' do
it 'includes name, username, avatar_url, and email' do it 'includes name, username, avatar_url, and email' do
user = create(:user) user = create(:user)
...@@ -4624,26 +4595,4 @@ describe User, :do_not_mock_admin_mode do ...@@ -4624,26 +4595,4 @@ describe User, :do_not_mock_admin_mode do
it_behaves_like 'does not require password to be present' it_behaves_like 'does not require password to be present'
end end
end end
describe '#human?' do
subject { user.human? }
let_it_be(:user) { create(:user) }
context 'when user is a human' do
before do
user.update(user_type: nil)
end
it { is_expected.to be true }
end
context 'when user is not a human' do
before do
user.update(user_type: 'alert_bot')
end
it { is_expected.to be false }
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe UserTypeEnums do
it '.types' do
expect(described_class.types.keys).to include('alert_bot', 'project_bot', 'human', 'ghost')
end
it '.bots' do
expect(described_class.bots.keys).to include('alert_bot', 'project_bot')
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