diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index bc338c305328d0fc5d46bf3eb8f38255d59e9c61..2fdbfb1ca22acb6491f96bdd3e3d728e3f8a4814 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -30,6 +30,7 @@ class Organization < ApplicationRecord has_one :organization_detail, inverse_of: :organization, autosave: true has_many :organization_users, inverse_of: :organization + has_many :user_aliases, inverse_of: :organization # if considering disable_joins on the below see: # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140343#note_1705047949 has_many :users, through: :organization_users, inverse_of: :organizations diff --git a/app/models/organizations/user_alias.rb b/app/models/organizations/user_alias.rb new file mode 100644 index 0000000000000000000000000000000000000000..1ea2a062c8f73024d873fba46acd3d733f9fdc22 --- /dev/null +++ b/app/models/organizations/user_alias.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Organizations + class UserAlias < ApplicationRecord + self.table_name = 'organizations_user_aliases' + + belongs_to :organization + belongs_to :user + + validates :username, presence: true, uniqueness: true + + scope :for_references, -> { includes(:organization, :user) } + scope :for_organization, ->(organization) { where(organization: organization) } + scope :with_usernames, ->(usernames_arg) { + usernames = usernames_arg.dup + first_clause = usernames.slice!(1) + relation = where('LOWER(username) = LOWER(?)', first_clause) + + usernames.inject(relation) do |relation, current_username| + relation.or(where('LOWER(username) = LOWER(?)', current_username)) + end + } + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 775abf2b7024b143de52f7dceadd88242925a8aa..c121cac728b2dcd9de3c878e6ed5fc2e5616eded 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -290,6 +290,7 @@ def update_tracked_fields!(request) belongs_to :created_by, class_name: 'User', optional: true has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user + has_many :organization_user_aliases, class_name: 'Organizations::UserAlias', inverse_of: :user has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users, disable_joins: true diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index a8e7d238414fdece1fb75bed5409282ac95e0f70..55f1d4ef6e30b950152a64d2c99c49cbee220083 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -73,6 +73,8 @@ def participant_as_hash(participant) group_as_hash(participant) when User user_as_hash(participant) + when Organizations::UserAlias + user_alias_as_hash(participant) else participant end @@ -88,6 +90,19 @@ def user_as_hash(user) } end + def user_alias_as_hash(user_alias) + user = user_alias.user + { + type: user.class.name, + username: user_alias.username, + name: user_alias.display_name, + avatar_url: user.avatar_url, + availability: lazy_user_availability(user).itself, # calling #itself to avoid returning a BatchLoader instance + aliased_username: user.username, + aliased_displayname: user.name + } + end + def group_as_hash(group) { type: group.class.name, diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 7e9dbce4e6d7a8e16750d0f8ce88d92ae621536f..306b17ea9dae95ab17bf79b2b55c18afc34f0d4d 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -12,6 +12,7 @@ def execute(noteable) noteable_owner + participants_in_noteable + all_members + + organization_user_aliases + project_members participants += groups unless relation_at_search_limit?(project_members) @@ -30,6 +31,10 @@ def all_members [{ username: "all", name: "All Project and Group Members", count: project_members_relation.count }] end + def organization_user_aliases + project.organization.user_aliases + end + def project_members_relation project.authorized_users end diff --git a/db/docs/organizations_user_aliases.yml b/db/docs/organizations_user_aliases.yml new file mode 100644 index 0000000000000000000000000000000000000000..364ba880c90c593b844a247052fc051143dc28df --- /dev/null +++ b/db/docs/organizations_user_aliases.yml @@ -0,0 +1,10 @@ +--- +table_name: organizations_user_aliases +classes: +- Organizations::UserAlias +feature_categories: +- todo +description: TODO +introduced_by_url: TODO +milestone: '17.11' +gitlab_schema: gitlab_main diff --git a/db/migrate/20250408025216_create_organizations_user_aliases.rb b/db/migrate/20250408025216_create_organizations_user_aliases.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee4f7fe12852494076b82298b862c5e355447857 --- /dev/null +++ b/db/migrate/20250408025216_create_organizations_user_aliases.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateOrganizationsUserAliases < Gitlab::Database::Migration[2.2] + milestone '17.11' + + def change + create_table :organizations_user_aliases do |t| # rubocop:disable Migration/EnsureFactoryForTable, Lint/RedundantCopDisableDirective -- file not correctly detected, works on commit, not on push, unclear why + t.belongs_to :organization, null: false + t.belongs_to :user, null: false + + # rubocop:disable Migration/PreventStrings -- these columns' type should be the same as User#username and User#name + t.string :username, null: false + t.string :display_name + # rubocop:enable Migration/PreventStrings + + t.timestamps_with_timezone null: false + + t.index [:organization_id, :user_id], unique: true, + name: :unique_organization_user_alias_organization_id_user_id + t.index [:organization_id, :username], unique: true, + name: :unique_organization_user_alias_organization_id_username + end + end +end diff --git a/db/schema_migrations/20250408025216 b/db/schema_migrations/20250408025216 new file mode 100644 index 0000000000000000000000000000000000000000..e983b38de4647eee3d95d78893cbb1f1125e4652 --- /dev/null +++ b/db/schema_migrations/20250408025216 @@ -0,0 +1 @@ +88fc85808b8459f538a6e7fe2e3a329ca16c48a93aa7f03368e690a8a290c66f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4962ba3d09476c572f710162fc463fac2172d8de..cc90d32e5e2c91c413f74d0f6bfcdfb15d948541 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18907,6 +18907,25 @@ CREATE SEQUENCE organizations_id_seq ALTER SEQUENCE organizations_id_seq OWNED BY organizations.id; +CREATE TABLE organizations_user_aliases ( + id bigint NOT NULL, + organization_id bigint NOT NULL, + user_id bigint NOT NULL, + username character varying NOT NULL, + display_name character varying, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE organizations_user_aliases_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE organizations_user_aliases_id_seq OWNED BY organizations_user_aliases.id; + CREATE SEQUENCE p_batched_git_ref_updates_deletions_id_seq START WITH 1 INCREMENT BY 1 @@ -27652,6 +27671,8 @@ ALTER TABLE ONLY organization_users ALTER COLUMN id SET DEFAULT nextval('organiz ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass); +ALTER TABLE ONLY organizations_user_aliases ALTER COLUMN id SET DEFAULT nextval('organizations_user_aliases_id_seq'::regclass); + ALTER TABLE ONLY p_batched_git_ref_updates_deletions ALTER COLUMN id SET DEFAULT nextval('p_batched_git_ref_updates_deletions_id_seq'::regclass); ALTER TABLE ONLY p_catalog_resource_component_usages ALTER COLUMN id SET DEFAULT nextval('p_catalog_resource_component_usages_id_seq'::regclass); @@ -30389,6 +30410,9 @@ ALTER TABLE ONLY organization_users ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id); +ALTER TABLE ONLY organizations_user_aliases + ADD CONSTRAINT organizations_user_aliases_pkey PRIMARY KEY (id); + ALTER TABLE ONLY p_batched_git_ref_updates_deletions ADD CONSTRAINT p_batched_git_ref_updates_deletions_pkey PRIMARY KEY (id, partition_id); @@ -36481,6 +36505,10 @@ CREATE INDEX index_organizations_on_path_trigram ON organizations USING gin (pat CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id); +CREATE INDEX index_organizations_user_aliases_on_organization_id ON organizations_user_aliases USING btree (organization_id); + +CREATE INDEX index_organizations_user_aliases_on_user_id ON organizations_user_aliases USING btree (user_id); + CREATE INDEX index_p_catalog_resource_component_usages_on_project_id ON ONLY p_catalog_resource_component_usages USING btree (project_id); CREATE INDEX index_p_catalog_resource_sync_events_on_id_where_pending ON ONLY p_catalog_resource_sync_events USING btree (id) WHERE (status = 1); @@ -38801,6 +38829,10 @@ CREATE INDEX unique_ml_model_versions_on_model_id_and_id ON ml_model_versions US CREATE UNIQUE INDEX unique_namespace_cluster_agent_mappings_for_agent_association ON namespace_cluster_agent_mappings USING btree (namespace_id, cluster_agent_id); +CREATE UNIQUE INDEX unique_organization_user_alias_organization_id_user_id ON organizations_user_aliases USING btree (organization_id, user_id); + +CREATE UNIQUE INDEX unique_organization_user_alias_organization_id_username ON organizations_user_aliases USING btree (organization_id, username); + CREATE UNIQUE INDEX unique_organizations_on_path_case_insensitive ON organizations USING btree (lower(path)); CREATE UNIQUE INDEX unique_packages_project_id_and_name_and_version_when_debian ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 9) AND (status <> 4)); diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb index bce181b6a882d10490ffacb37dbd8c45fce4b6a2..863fd3b19eaf1ac18c760197ae9fb65a7459415f 100644 --- a/lib/banzai/filter/references/user_reference_filter.rb +++ b/lib/banzai/filter/references/user_reference_filter.rb @@ -49,7 +49,9 @@ def object_link_filter(text, pattern, link_content: nil, link_reference: false) link_to_all(link_content: link_content) else cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do - if namespace = namespaces[username.downcase] + if user_alias = user_aliases[username.downcase] + link_to_user_alias(user_alias) + elsif namespace = namespaces[username.downcase] link_to_namespace(namespace, link_content: link_content) || match else match @@ -71,6 +73,14 @@ def namespaces .transform_keys(&:downcase) end + def user_aliases + @user_aliases ||= Organizations::UserAlias.for_references + .for_organization(Organizations::Organization.first) + .with_usernames(usernames) + .index_by(&:username) + .transform_keys(&:downcase) + end + # Returns all usernames referenced in the current document. def usernames refs = Set.new @@ -118,6 +128,15 @@ def link_to_group(group, namespace, link_content: nil) link_tag(url, data, content, namespace.full_name) end + def link_to_user_alias(user_alias, link_content: nil) + user = user_alias.user + url = urls.user_url(user, only_path: context[:only_path]) + data = data_attribute(user: user) + content = link_content || (User.reference_prefix + user_alias.username) + + link_tag(url, data, content, user_alias.username) + end + def link_to_user(user, namespace, link_content: nil) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) diff --git a/lib/users/internal.rb b/lib/users/internal.rb index cc38f6517cd62c8194a1ee79d32290a495a09235..9a0a9a0d54f70fe11e4e0bf5f7e637478a9d145e 100644 --- a/lib/users/internal.rb +++ b/lib/users/internal.rb @@ -3,24 +3,29 @@ module Users class Internal class << self - include Gitlab::Utils::StrongMemoize - # rubocop:disable CodeReuse/ActiveRecord - # Return (create if necessary) the ghost user. The ghost user # owns records previously belonging to deleted users. - def ghost + def ghost(organization = nil) + organization = fetch_organization(organization) + email = 'ghost%s@example.com' - unique_internal(User.where(user_type: :ghost), 'ghost', email) do |u| + unique_internal(user_type: :ghost, username: 'ghost', organization: organization, email_pattern: email) do |u| u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have ' \ 'since been deleted. This user cannot be removed.') u.name = 'Ghost User' end end - def alert_bot + def alert_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "alert%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :alert_bot), 'alert-bot', email_pattern) do |u| + unique_internal( + user_type: :alert_bot, + username: 'alert-bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'The GitLab alert bot' u.name = 'GitLab Alert Bot' u.avatar = bot_avatar(image: 'alert-bot.png') @@ -29,10 +34,16 @@ def alert_bot end end - def migration_bot + def migration_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u| + unique_internal( + user_type: :migration_bot, + username: 'migration-bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'The GitLab migration bot' u.name = 'GitLab Migration Bot' u.confirmed_at = Time.zone.now @@ -40,10 +51,16 @@ def migration_bot end end - def security_bot + def security_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "security-bot%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :security_bot), 'GitLab-Security-Bot', email_pattern) do |u| + unique_internal( + user_type: :security_bot, + username: 'GitLab-Security-Bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'System bot that monitors detected vulnerabilities for solutions ' \ 'and creates merge requests with the fixes.' u.name = 'GitLab Security Bot' @@ -53,10 +70,16 @@ def security_bot end end - def support_bot + def support_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "support%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :support_bot), 'support-bot', email_pattern) do |u| + unique_internal( + user_type: :support_bot, + username: 'support-bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'The GitLab support bot used for Service Desk' u.name = 'GitLab Support Bot' u.avatar = bot_avatar(image: 'support-bot.png') @@ -68,19 +91,20 @@ def support_bot # Checks against this bot are now included in every issue and work item # detail and list page rendering and in GraphQL queries (especially for determining # the web_url of an issue/work item). - # Because the bot never changes once created, we can memoize it for - # the lifetime of the application process. It also doesn't matter that - # different nodes may have different object instances of the bot. - # We only memoize the id because this is the information we check against. - def support_bot_id - support_bot.id + def support_bot_id(organization = nil) + support_bot(organization).id end - strong_memoize_attr :support_bot_id - def automation_bot + def automation_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "automation%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :automation_bot), 'automation-bot', email_pattern) do |u| + unique_internal( + user_type: :automation_bot, + username: 'automation-bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'The GitLab automation bot used for automated workflows and tasks' u.name = 'GitLab Automation Bot' u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for automation-bot @@ -89,10 +113,16 @@ def automation_bot end end - def llm_bot + def llm_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "llm-bot%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :llm_bot), 'GitLab-Llm-Bot', email_pattern) do |u| + unique_internal( + user_type: :llm_bot, + username: 'GitLab-Llm-Bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'The Gitlab LLM bot used for fetching LLM-generated content' u.name = 'GitLab LLM Bot' u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for llm-bot @@ -101,10 +131,16 @@ def llm_bot end end - def duo_code_review_bot + def duo_code_review_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "gitlab-duo%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :duo_code_review_bot), 'GitLabDuo', email_pattern) do |u| + unique_internal( + user_type: :duo_code_review_bot, + username: 'GitLabDuo', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'GitLab Duo bot for handling AI tasks' u.name = 'GitLab Duo' u.avatar = bot_avatar(image: 'duo-bot.png') @@ -113,10 +149,16 @@ def duo_code_review_bot end end - def admin_bot + def admin_bot(organization = nil) + organization = fetch_organization(organization) email_pattern = "admin-bot%s@#{Settings.gitlab.host}" - unique_internal(User.where(user_type: :admin_bot), 'GitLab-Admin-Bot', email_pattern) do |u| + unique_internal( + user_type: :admin_bot, + username: 'GitLab-Admin-Bot', + organization: organization, + email_pattern: email_pattern + ) do |u| u.bio = 'Admin bot used for tasks that require admin privileges' u.name = 'GitLab Admin Bot' u.avatar = bot_avatar(image: 'admin-bot.png') @@ -126,24 +168,41 @@ def admin_bot end end - # rubocop:enable CodeReuse/ActiveRecord - def bot_avatar(image:) Rails.root.join('lib', 'assets', 'images', 'bot_avatars', image).open end private + # rubocop:disable CodeReuse/ActiveRecord + + def fetch_organization(passed_object) + return passed_object if passed_object.is_a?(Organizations::Organization) + + Gitlab::AppLogger.info( + "Internal user requested without organization" + ) + + Organizations::Organization.default_organization + end + # NOTE: This method is patched in spec/spec_helper.rb to allow use of exclusive lease in RSpec's # :before_all scope to keep the specs DRY. - def unique_internal(scope, username, email_pattern, &block) - scope.first || create_unique_internal(scope, username, email_pattern, &block) + def unique_internal(user_type:, username:, email_pattern:, organization:, &block) + organization.users.where(user_type: user_type).first || + create_unique_internal( + user_type: user_type, + username: username, + email_pattern: email_pattern, + organization: organization, + &block + ) end - def create_unique_internal(scope, username, email_pattern, &creation_block) + def create_unique_internal(user_type:, username:, email_pattern:, organization:, &creation_block) # Since we only want a single one of these in an instance, we use an # exclusive lease to ensure than this block is never run concurrently. - lease_key = "user:unique_internal:#{username}" + lease_key = "user:unique_internal:#{username}:#{organization.id}" lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) uuid = lease.try_obtain @@ -157,33 +216,41 @@ def create_unique_internal(scope, username, email_pattern, &creation_block) # Recheck if the user is already present. One might have been # added between the time we last checked (first line of this method) # and the time we acquired the lock. + scope = organization.users.where(user_type: user_type) existing_user = scope.model.uncached { scope.first } return existing_user if existing_user.present? uniquify = Gitlab::Utils::Uniquify.new - username = uniquify.string(username) { |s| Namespace.by_path(s) } + unique_username = uniquify.string(username) { |s| Namespace.by_path(s) } email = uniquify.string(->(n) { Kernel.sprintf(email_pattern, n) }) do |s| User.find_by_email(s) end - user = scope.build( - username: username, + user = User.where(user_type: user_type).build( + username: unique_username, email: email, &creation_block ) - # https://gitlab.com/gitlab-org/gitlab/-/issues/442780 - organization = ::Organizations::Organization.first user.assign_personal_namespace(organization) user.organizations << organization Users::UpdateService.new(user, user: user).execute(validate: false) + Organizations::UserAlias.find_or_create_by( + organization: organization, + user: user, + username: username, + display_name: user.name + ) + user ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end + + # rubocop:enable CodeReuse/ActiveRecord end end end diff --git a/spec/factories/organizations/user_aliases.rb b/spec/factories/organizations/user_aliases.rb new file mode 100644 index 0000000000000000000000000000000000000000..bf3d893ae5302080a9bc639d357ce0847881a60a --- /dev/null +++ b/spec/factories/organizations/user_aliases.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :organizations_user_alias, class: 'Organizations::UserAlias' do + user + organization + sequence(:username) { |n| "organization_user_alias_#{n}" } + display_name { username.humanize } + end +end diff --git a/spec/lib/users/internal_spec.rb b/spec/lib/users/internal_spec.rb index 0e8ab182a3f0c5522a3e8edaaf1d72a75d5b3970..5f277b0b3e00f969ec0f6a5b0bea762ffc3b5469 100644 --- a/spec/lib/users/internal_spec.rb +++ b/spec/lib/users/internal_spec.rb @@ -4,6 +4,11 @@ RSpec.describe Users::Internal, feature_category: :user_profile do let_it_be(:organization) { create(:organization) } + let_it_be(:organization2) { create(:organization) } + + before do + allow(Organizations::Organization).to receive(:default_organization).and_return(organization) + end shared_examples 'bot users' do |bot_type, username, email| it 'creates the user if it does not exist' do @@ -19,7 +24,7 @@ expect(bot_user.namespace.organization).to eq(organization) end - it 'assigns the first found organization to the created user' do + it 'assigns the organization to the created user' do bot_user = described_class.public_send(bot_type) expect(bot_user.organizations.first).to eq(organization) @@ -33,6 +38,54 @@ end.not_to change { User.count } end + context 'when accessed with an organization' do + it 'creates the user if it does not exist for that organization' do + described_class.public_send(bot_type, organization) + + expect do + described_class.public_send(bot_type, organization2) + end.to change { User.where(user_type: bot_type).count }.by(1) + end + + it 'creates organization username alias for user' do + expect do + described_class.public_send(bot_type, organization2) + end.to change { Organizations::UserAlias.count }.by(1) + end + + it 'creates a unique user per organization' do + user1 = described_class.public_send(bot_type, organization) + user2 = described_class.public_send(bot_type, organization2) + + expect(user1).not_to eq(user2) + expect(user1.username).not_to eq(user2.username) + expect(user1.email).not_to eq(user2.email) + expect(user1.full_path).not_to eq(user2.full_path) + end + + it 'creates a route for the namespace of the created user' do + bot_user = described_class.public_send(bot_type, organization2) + + expect(bot_user.namespace.route).to be_present + expect(bot_user.namespace.organization).to eq(organization2) + end + + it 'assigns the organization to the created user' do + bot_user = described_class.public_send(bot_type, organization2) + + expect(bot_user.organizations.first).to eq(organization2) + expect(bot_user.organizations).not_to include(organization) + end + + it 'does not create a new user if it already exists' do + described_class.public_send(bot_type, organization2) + + expect do + described_class.public_send(bot_type, organization2) + end.not_to change { User.count } + end + end + context 'when a regular user exists with the bot username' do it 'creates a user with a non-conflicting username' do create(:user, username: username) @@ -117,13 +170,6 @@ end describe '.support_bot_id' do - before do - # Ensure support bot user is created and memoization uses the same id - # See https://gitlab.com/gitlab-org/gitlab/-/issues/509629 - described_class.clear_memoization(:support_bot_id) - described_class.support_bot_id - end - subject { described_class.support_bot_id } it { is_expected.to eq(described_class.support_bot.id) } diff --git a/spec/models/organizations/user_aliases_spec.rb b/spec/models/organizations/user_aliases_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..23679fe00a4d762436d5e90236e6c0fd51980f77 --- /dev/null +++ b/spec/models/organizations/user_aliases_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::UserAlias, type: :model, feature_category: :cell do + let_it_be(:organization) { create(:organization) } + let_it_be(:user) { create(:user, organization: organization) } + let_it_be(:user_alias) { create(:organizations_user_alias, organization: organization, user: user) } + + it 'is valid' do + expect(user_alias).to be_valid + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 58f0b40b33e6fc1fc745d17f6b60b6fa96287c90..2adc3bd0c1ada2e7bbf43acd3ccc8e7a5acbb4cd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -549,13 +549,6 @@ self.class.use_transactional_tests = true end end - - config.before(:context) do - # Clear support bot user memoization because it's created - # a lot of times in our test suite and ids mighht not match any more. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/509629 - Users::Internal.clear_memoization(:support_bot_id) - end end # Disabled because it's causing N+1 queries. @@ -613,7 +606,7 @@ module UsersInternalAllowExclusiveLease extend ActiveSupport::Concern class_methods do - def unique_internal(scope, username, email_pattern, &block) + def unique_internal(user_type:, username:, email_pattern:, organization:, &block) # this lets skip transaction checks when Users::Internal bots are created in # let_it_be blocks during test set-up. # @@ -629,7 +622,7 @@ def unique_internal(scope, username, email_pattern, &block) # first organization as a temporary workaround. Many specs lack an organization in the database, causing foreign key # constraint violations when creating internal users. We're not seeding organizations before all specs for # performance. - def create_unique_internal(scope, username, email_pattern, &creation_block) + def create_unique_internal(user_type:, username:, email_pattern:, organization:, &block) Organizations::Organization.first || FactoryBot.create(:organization) super