Commit 79e4ed28 authored by Alexandru Croitor's avatar Alexandru Croitor Committed by Imre Farkas

Store mentioned users, groups, projects in DB

When a note is created or updated it gets parsed and
users, groups, projects mentioned in the note are stored in respective
db table, or deleted when note is deleted.
parent 7e262371
...@@ -281,6 +281,10 @@ class Commit ...@@ -281,6 +281,10 @@ class Commit
project.notes.for_commit_id(self.id) project.notes.for_commit_id(self.id)
end end
def user_mentions
CommitUserMention.where(commit_id: self.id)
end
def discussion_notes def discussion_notes
notes.non_diff_notes notes.non_diff_notes
end end
......
# frozen_string_literal: true
class CommitUserMention < UserMention
belongs_to :note
end
...@@ -80,6 +80,66 @@ module Mentionable ...@@ -80,6 +80,66 @@ module Mentionable
all_references(current_user).users all_references(current_user).users
end end
def store_mentions!
# if store_mentioned_users_to_db feature flag is not enabled then consider storing operation as succeeded
# because we wrap this method in transaction with with_transaction_returning_status, and we need the status to be
# successful if mentionable.save is successful.
#
# This line will get removed when we remove the feature flag.
return true unless store_mentioned_users_to_db_enabled?
refs = all_references(self.author)
references = {}
references[:mentioned_users_ids] = refs.mentioned_users&.pluck(:id).presence
references[:mentioned_groups_ids] = refs.mentioned_groups&.pluck(:id).presence
references[:mentioned_projects_ids] = refs.mentioned_projects&.pluck(:id).presence
# One retry should be enough as next time `model_user_mention` should return the existing mention record, that
# threw the `ActiveRecord::RecordNotUnique` exception in first place.
self.class.safe_ensure_unique(retries: 1) do
user_mention = model_user_mention
user_mention.mentioned_users_ids = references[:mentioned_users_ids]
user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
if user_mention.has_mentions?
user_mention.save!
elsif user_mention.persisted?
user_mention.destroy!
end
true
end
end
def referenced_users
User.where(id: user_mentions.select("unnest(mentioned_users_ids)"))
end
def referenced_projects(current_user = nil)
Project.where(id: user_mentions.select("unnest(mentioned_projects_ids)")).public_or_visible_to_user(current_user)
end
def referenced_project_users(current_user = nil)
User.joins(:project_members).where(members: { source_id: referenced_projects(current_user) }).distinct
end
def referenced_groups(current_user = nil)
# TODO: IMPORTANT: Revisit before using it.
# Check DB data for max mentioned groups per mentionable:
#
# select issue_id, count(mentions_count.men_gr_id) gr_count from
# (select DISTINCT unnest(mentioned_groups_ids) as men_gr_id, issue_id
# from issue_user_mentions group by issue_id, mentioned_groups_ids) as mentions_count
# group by mentions_count.issue_id order by gr_count desc limit 10
Group.where(id: user_mentions.select("unnest(mentioned_groups_ids)")).public_or_visible_to_user(current_user)
end
def referenced_group_users(current_user = nil)
User.joins(:group_members).where(members: { source_id: referenced_groups }).distinct
end
def directly_addressed_users(current_user = nil) def directly_addressed_users(current_user = nil)
all_references(current_user).directly_addressed_users all_references(current_user).directly_addressed_users
end end
...@@ -171,6 +231,26 @@ module Mentionable ...@@ -171,6 +231,26 @@ module Mentionable
def mentionable_params def mentionable_params
{} {}
end end
# User mention that is parsed from model description rather then its related notes.
# Models that have a descriprion attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
# a description attribute.
#
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
# in a multithreaded environment. Make sure to use it within a *safe_ensure_unique* block.
def model_user_mention
user_mentions.where(note_id: nil).first_or_initialize
end
# We need this method to be checking that store_mentioned_users_to_db feature flag is enabled at the group level
# and not the project level as epics are defined at group level and we want to have epics store user mentions as well
# for the test period.
# During the test period the flag should be enabled at the group level.
def store_mentioned_users_to_db_enabled?
return Feature.enabled?(:store_mentioned_users_to_db, self.project&.group) if self.respond_to?(:project)
return Feature.enabled?(:store_mentioned_users_to_db, self.group) if self.respond_to?(:group)
end
end end
Mentionable.prepend_if_ee('EE::Mentionable') Mentionable.prepend_if_ee('EE::Mentionable')
...@@ -42,6 +42,7 @@ class Issue < ApplicationRecord ...@@ -42,6 +42,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings has_many :zoom_meetings
has_many :user_mentions, class_name: "IssueUserMention"
has_one :sentry_issue has_one :sentry_issue
validates :project, presence: true validates :project, presence: true
......
# frozen_string_literal: true
class IssueUserMention < UserMention
belongs_to :issue
belongs_to :note
end
...@@ -71,6 +71,7 @@ class MergeRequest < ApplicationRecord ...@@ -71,6 +71,7 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_assignees has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees has_many :assignees, class_name: "User", through: :merge_request_assignees
has_many :user_mentions, class_name: "MergeRequestUserMention"
has_many :deployment_merge_requests has_many :deployment_merge_requests
......
# frozen_string_literal: true
class MergeRequestUserMention < UserMention
belongs_to :merge_request
belongs_to :note
end
...@@ -499,8 +499,18 @@ class Note < ApplicationRecord ...@@ -499,8 +499,18 @@ class Note < ApplicationRecord
project project
end end
def user_mentions
noteable.user_mentions.where(note: self)
end
private private
# Using this method followed by a call to `save` may result in ActiveRecord::RecordNotUnique exception
# in a multithreaded environment. Make sure to use it within a `safe_ensure_unique` block.
def model_user_mention
user_mentions.first_or_initialize
end
def system_note_viewable_by?(user) def system_note_viewable_by?(user)
return true unless system_note_metadata return true unless system_note_metadata
......
...@@ -37,6 +37,7 @@ class Snippet < ApplicationRecord ...@@ -37,6 +37,7 @@ class Snippet < ApplicationRecord
belongs_to :project belongs_to :project
has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "SnippetUserMention"
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
...@@ -69,6 +70,8 @@ class Snippet < ApplicationRecord ...@@ -69,6 +70,8 @@ class Snippet < ApplicationRecord
scope :inc_author, -> { includes(:author) } scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) } scope :inc_relations_for_view, -> { includes(author: :status) }
attr_mentionable :description
participant :author participant :author
participant :notes_with_associations participant :notes_with_associations
......
# frozen_string_literal: true
class SnippetUserMention < UserMention
belongs_to :snippet
belongs_to :note
end
# frozen_string_literal: true
class UserMention < ApplicationRecord
self.abstract_class = true
def has_mentions?
mentioned_users_ids.present? || mentioned_groups_ids.present? || mentioned_projects_ids.present?
end
private
def mentioned_users
User.where(id: mentioned_users_ids)
end
def mentioned_groups
Group.where(id: mentioned_groups_ids)
end
def mentioned_projects
Project.where(id: mentioned_projects_ids)
end
end
...@@ -21,7 +21,11 @@ class CreateSnippetService < BaseService ...@@ -21,7 +21,11 @@ class CreateSnippetService < BaseService
spam_check(snippet, current_user) spam_check(snippet, current_user)
if snippet.save snippet_saved = snippet.with_transaction_returning_status do
snippet.save && snippet.store_mentions!
end
if snippet_saved
UserAgentDetailService.new(snippet, @request).create UserAgentDetailService.new(snippet, @request).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create) Gitlab::UsageDataCounters::SnippetCounter.count(:create)
end end
......
...@@ -163,7 +163,11 @@ class IssuableBaseService < BaseService ...@@ -163,7 +163,11 @@ class IssuableBaseService < BaseService
before_create(issuable) before_create(issuable)
if issuable.save issuable_saved = issuable.with_transaction_returning_status do
issuable.save && issuable.store_mentions!
end
if issuable_saved
Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false) Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false)
after_create(issuable) after_create(issuable)
...@@ -224,7 +228,11 @@ class IssuableBaseService < BaseService ...@@ -224,7 +228,11 @@ class IssuableBaseService < BaseService
update_project_counters = issuable.project && update_project_counter_caches?(issuable) update_project_counters = issuable.project && update_project_counter_caches?(issuable)
ensure_milestone_available(issuable) ensure_milestone_available(issuable)
if issuable.with_transaction_returning_status { issuable.save(touch: should_touch) } issuable_saved = issuable.with_transaction_returning_status do
issuable.save(touch: should_touch) && issuable.store_mentions!
end
if issuable_saved
Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels]) Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels])
handle_changes(issuable, old_associations: old_associations) handle_changes(issuable, old_associations: old_associations)
......
...@@ -33,7 +33,11 @@ module Notes ...@@ -33,7 +33,11 @@ module Notes
NewNoteWorker.perform_async(note.id) NewNoteWorker.perform_async(note.id)
end end
if !only_commands && note.save note_saved = note.with_transaction_returning_status do
!only_commands && note.save && note.store_mentions!
end
if note_saved
if note.part_of_discussion? && note.discussion.can_convert_to_discussion? if note.part_of_discussion? && note.discussion.can_convert_to_discussion?
note.discussion.convert_to_discussion!(save: true) note.discussion.convert_to_discussion!(save: true)
end end
......
...@@ -7,7 +7,11 @@ module Notes ...@@ -7,7 +7,11 @@ module Notes
old_mentioned_users = note.mentioned_users(current_user).to_a old_mentioned_users = note.mentioned_users(current_user).to_a
note.update(params.merge(updated_by: current_user)) note.assign_attributes(params.merge(updated_by: current_user))
note.with_transaction_returning_status do
note.save && note.store_mentions!
end
only_commands = false only_commands = false
......
...@@ -25,8 +25,12 @@ class UpdateSnippetService < BaseService ...@@ -25,8 +25,12 @@ class UpdateSnippetService < BaseService
snippet.assign_attributes(params) snippet.assign_attributes(params)
spam_check(snippet, current_user) spam_check(snippet, current_user)
snippet.save.tap do |succeeded| snippet_saved = snippet.with_transaction_returning_status do
Gitlab::UsageDataCounters::SnippetCounter.count(:update) if succeeded snippet.save && snippet.store_mentions!
end
if snippet_saved
Gitlab::UsageDataCounters::SnippetCounter.count(:update)
end end
end end
end end
---
title: Store users, groups, projects mentioned in Markdown to DB tables
merge_request: 19088
author:
type: added
...@@ -7,6 +7,7 @@ module DesignManagement ...@@ -7,6 +7,7 @@ module DesignManagement
include Gitlab::FileTypeDetection include Gitlab::FileTypeDetection
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include Referable include Referable
include Mentionable
belongs_to :project, inverse_of: :designs belongs_to :project, inverse_of: :designs
belongs_to :issue belongs_to :issue
...@@ -16,6 +17,7 @@ module DesignManagement ...@@ -16,6 +17,7 @@ module DesignManagement
# This is a polymorphic association, so we can't count on FK's to delete the # This is a polymorphic association, so we can't count on FK's to delete the
# data # data
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "DesignUserMention"
validates :project, :filename, presence: true validates :project, :filename, presence: true
validates :issue, presence: true, unless: :importing? validates :issue, presence: true, unless: :importing?
......
# frozen_string_literal: true
class DesignUserMention < UserMention
belongs_to :design, class_name: 'DesignManagement::Design'
belongs_to :note
end
...@@ -51,6 +51,7 @@ module EE ...@@ -51,6 +51,7 @@ module EE
has_many :epic_issues has_many :epic_issues
has_many :issues, through: :epic_issues has_many :issues, through: :epic_issues
has_many :user_mentions, class_name: "EpicUserMention"
validates :group, presence: true validates :group, presence: true
validate :validate_parent, on: :create validate :validate_parent, on: :create
......
# frozen_string_literal: true
class EpicUserMention < UserMention
belongs_to :epic
belongs_to :note
end
...@@ -23,4 +23,8 @@ class Review < ApplicationRecord ...@@ -23,4 +23,8 @@ class Review < ApplicationRecord
ext ext
end end
def user_mentions
merge_request.user_mentions.where.not(note_id: nil)
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Epic, 'Mentionable' do
describe '#store_mentions!' do
it_behaves_like 'mentions in description', :epic
it_behaves_like 'mentions in notes', :epic do
let(:note) { create(:note_on_epic) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :epic do
let(:note) { create(:note_on_epic) }
let(:mentionable) { note.noteable }
end
end
end
describe DesignManagement::Design do
describe '#store_mentions!' do
it_behaves_like 'mentions in notes', :design do
let(:note) { create(:diff_note_on_design) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :design do
let(:note) { create(:diff_note_on_design) }
let(:mentionable) { note.noteable }
end
end
end
...@@ -17,6 +17,7 @@ describe DesignManagement::Design do ...@@ -17,6 +17,7 @@ describe DesignManagement::Design do
it { is_expected.to have_many(:actions) } it { is_expected.to have_many(:actions) }
it { is_expected.to have_many(:versions) } it { is_expected.to have_many(:versions) }
it { is_expected.to have_many(:notes).dependent(:delete_all) } it { is_expected.to have_many(:notes).dependent(:delete_all) }
it { is_expected.to have_many(:user_mentions) }
end end
describe 'validations' do describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe DesignUserMention do
describe 'associations' do
it { is_expected.to belong_to(:design) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
...@@ -16,6 +16,7 @@ describe Epic do ...@@ -16,6 +16,7 @@ describe Epic do
it { is_expected.to belong_to(:parent) } it { is_expected.to belong_to(:parent) }
it { is_expected.to have_many(:epic_issues) } it { is_expected.to have_many(:epic_issues) }
it { is_expected.to have_many(:children) } it { is_expected.to have_many(:children) }
it { is_expected.to have_many(:user_mentions).class_name("EpicUserMention") }
end end
describe 'validations' do describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe EpicUserMention do
describe 'associations' do
it { is_expected.to belong_to(:epic) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Banzai module Banzai
module ReferenceParser module ReferenceParser
class MentionedUsersByGroupParser < BaseParser class MentionedGroupParser < BaseParser
GROUP_ATTR = 'data-group' GROUP_ATTR = 'data-group'
self.reference_type = :user self.reference_type = :user
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Banzai module Banzai
module ReferenceParser module ReferenceParser
class MentionedUsersByProjectParser < ProjectParser class MentionedProjectParser < ProjectParser
PROJECT_ATTR = 'data-project' PROJECT_ATTR = 'data-project'
self.reference_type = :user self.reference_type = :user
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor class ReferenceExtractor < Banzai::ReferenceExtractor
REFERABLES = %i(user issue label milestone REFERABLES = %i(user issue label milestone mentioned_user mentioned_group mentioned_project
merge_request snippet commit commit_range directly_addressed_user epic).freeze merge_request snippet commit commit_range directly_addressed_user epic).freeze
attr_accessor :project, :current_user, :author attr_accessor :project, :current_user, :author
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Banzai::ReferenceParser::MentionedUsersByGroupParser do describe Banzai::ReferenceParser::MentionedGroupParser do
include ReferenceParserHelpers include ReferenceParserHelpers
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Banzai::ReferenceParser::MentionedUsersByProjectParser do describe Banzai::ReferenceParser::MentionedProjectParser do
include ReferenceParserHelpers include ReferenceParserHelpers
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
......
...@@ -34,6 +34,7 @@ issues: ...@@ -34,6 +34,7 @@ issues:
- zoom_meetings - zoom_meetings
- vulnerability_links - vulnerability_links
- related_vulnerabilities - related_vulnerabilities
- user_mentions
events: events:
- author - author
- project - project
...@@ -82,6 +83,7 @@ snippets: ...@@ -82,6 +83,7 @@ snippets:
- notes - notes
- award_emoji - award_emoji
- user_agent_detail - user_agent_detail
- user_mentions
releases: releases:
- author - author
- project - project
...@@ -142,6 +144,7 @@ merge_requests: ...@@ -142,6 +144,7 @@ merge_requests:
- description_versions - description_versions
- deployment_merge_requests - deployment_merge_requests
- deployments - deployments
- user_mentions
external_pull_requests: external_pull_requests:
- project - project
merge_request_diff: merge_request_diff:
...@@ -539,6 +542,7 @@ design: &design ...@@ -539,6 +542,7 @@ design: &design
- actions - actions
- versions - versions
- notes - notes
- user_mentions
designs: *design designs: *design
actions: actions:
- design - design
......
...@@ -166,6 +166,21 @@ describe Issue, "Mentionable" do ...@@ -166,6 +166,21 @@ describe Issue, "Mentionable" do
create(:issue, project: project, description: description, author: author) create(:issue, project: project, description: description, author: author)
end end
end end
describe '#store_mentions!' do
it_behaves_like 'mentions in description', :issue
it_behaves_like 'mentions in notes', :issue do
let(:note) { create(:note_on_issue) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :issue do
let(:note) { create(:note_on_issue) }
let(:mentionable) { note.noteable }
end
end
end end
describe Commit, 'Mentionable' do describe Commit, 'Mentionable' do
...@@ -221,4 +236,56 @@ describe Commit, 'Mentionable' do ...@@ -221,4 +236,56 @@ describe Commit, 'Mentionable' do
end end
end end
end end
describe '#store_mentions!' do
it_behaves_like 'mentions in notes', :commit do
let(:note) { create(:note_on_commit) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :commit do
let(:note) { create(:note_on_commit) }
let(:mentionable) { note.noteable }
end
end
end
describe MergeRequest, 'Mentionable' do
describe '#store_mentions!' do
it_behaves_like 'mentions in description', :merge_request
it_behaves_like 'mentions in notes', :merge_request do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note) { create(:note_on_merge_request, noteable: merge_request, project: merge_request.project) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :merge_request do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note) { create(:note_on_merge_request, noteable: merge_request, project: merge_request.project) }
let(:mentionable) { note.noteable }
end
end
end
describe Snippet, 'Mentionable' do
describe '#store_mentions!' do
it_behaves_like 'mentions in description', :project_snippet
it_behaves_like 'mentions in notes', :project_snippet do
let(:note) { create(:note_on_project_snippet) }
let(:mentionable) { note.noteable }
end
end
describe 'load mentions' do
it_behaves_like 'load mentions from DB', :project_snippet do
let(:note) { create(:note_on_project_snippet) }
let(:mentionable) { note.noteable }
end
end
end end
...@@ -12,6 +12,7 @@ describe Issue do ...@@ -12,6 +12,7 @@ describe Issue do
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') } it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
it { is_expected.to belong_to(:closed_by).class_name('User') } it { is_expected.to belong_to(:closed_by).class_name('User') }
it { is_expected.to have_many(:assignees) } it { is_expected.to have_many(:assignees) }
it { is_expected.to have_many(:user_mentions).class_name("IssueUserMention") }
it { is_expected.to have_one(:sentry_issue) } it { is_expected.to have_one(:sentry_issue) }
end end
......
...@@ -17,6 +17,7 @@ describe MergeRequest do ...@@ -17,6 +17,7 @@ describe MergeRequest do
it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) } it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) } it { is_expected.to have_many(:merge_request_diffs) }
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
context 'for forks' do context 'for forks' do
let!(:project) { create(:project) } let!(:project) { create(:project) }
......
...@@ -18,6 +18,7 @@ describe Snippet do ...@@ -18,6 +18,7 @@ describe Snippet do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") }
end end
describe 'validation' do describe 'validation' do
......
# frozen_string_literal: true
require 'spec_helper'
describe CommitUserMention do
describe 'associations' do
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
# frozen_string_literal: true
require 'spec_helper'
describe IssueUserMention do
describe 'associations' do
it { is_expected.to belong_to(:issue) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
# frozen_string_literal: true
require 'spec_helper'
describe MergeRequestUserMention do
describe 'associations' do
it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
# frozen_string_literal: true
require 'spec_helper'
describe SnippetUserMention do
describe 'associations' do
it { is_expected.to belong_to(:snippet) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
...@@ -195,3 +195,153 @@ shared_examples 'an editable mentionable' do ...@@ -195,3 +195,153 @@ shared_examples 'an editable mentionable' do
subject.create_new_cross_references!(author) subject.create_new_cross_references!(author)
end end
end end
shared_examples_for 'mentions in description' do |mentionable_type|
describe 'when store_mentioned_users_to_db feature disabled' do
before do
stub_feature_flags(store_mentioned_users_to_db: false)
mentionable.store_mentions!
end
context 'when mentionable description contains mentions' do
let(:user) { create(:user) }
let(:mentionable) { create(mentionable_type, description: "#{user.to_reference} some description") }
it 'stores no mentions' do
expect(mentionable.user_mentions.count).to eq 0
end
end
end
describe 'when store_mentioned_users_to_db feature enabled' do
before do
stub_feature_flags(store_mentioned_users_to_db: true)
mentionable.store_mentions!
end
context 'when mentionable description has no mentions' do
let(:mentionable) { create(mentionable_type, description: "just some description") }
it 'stores no mentions' do
expect(mentionable.user_mentions.count).to eq 0
end
end
context 'when mentionable description contains mentions' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:mentionable_desc) { "#{user.to_reference} some description #{group.to_reference(full: true)} and @all" }
let(:mentionable) { create(mentionable_type, description: mentionable_desc) }
it 'stores mentions' do
add_member(user)
expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to match_array([user])
expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to match_array([group])
end
end
end
end
shared_examples_for 'mentions in notes' do |mentionable_type|
context 'when mentionable notes contain mentions' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:note_desc) { "#{user.to_reference} and #{group.to_reference(full: true)} and @all" }
let!(:mentionable) { note.noteable }
before do
note.update(note: note_desc)
note.store_mentions!
add_member(user)
end
it 'returns all mentionable mentions' do
expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to eq [user]
expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to eq [group]
end
end
end
shared_examples_for 'load mentions from DB' do |mentionable_type|
context 'load stored mentions' do
let_it_be(:user) { create(:user) }
let_it_be(:mentioned_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:note_desc) { "#{mentioned_user.to_reference} and #{group.to_reference(full: true)} and @all" }
before do
note.update(note: note_desc)
note.store_mentions!
add_member(user)
end
context 'when stored user mention contains ids of inexistent records' do
before do
user_mention = note.send(:model_user_mention)
mention_ids = {
mentioned_users_ids: user_mention.mentioned_users_ids.to_a << User.maximum(:id).to_i.succ,
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << Project.maximum(:id).to_i.succ,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << Group.maximum(:id).to_i.succ
}
user_mention.update(mention_ids)
end
it 'filters out inexistent mentions' do
expect(mentionable.referenced_users).to match_array([mentioned_user])
expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to match_array([group])
end
end
context 'when private projects and groups are mentioned' do
let(:mega_user) { create(:user) }
let(:private_project) { create(:project, :private) }
let(:project_member) { create(:project_member, user: create(:user), project: private_project) }
let(:private_group) { create(:group, :private) }
let(:group_member) { create(:group_member, user: create(:user), group: private_group) }
before do
user_mention = note.send(:model_user_mention)
mention_ids = {
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
}
user_mention.update(mention_ids)
add_member(mega_user)
private_project.add_developer(mega_user)
private_group.add_developer(mega_user)
end
context 'when user has no access to some mentions' do
it 'filters out inaccessible mentions' do
expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to match_array([group])
end
end
context 'when user has access to all mentions' do
it 'returns all mentions' do
expect(mentionable.referenced_projects(mega_user)).to match_array([mentionable.project, private_project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(mega_user)).to match_array([group, private_group])
end
end
end
end
end
def add_member(user)
issuable_parent = if mentionable.is_a?(Epic)
mentionable.group
else
mentionable.project
end
issuable_parent&.add_developer(user)
end
# frozen_string_literal: true
require 'spec_helper'
shared_examples_for 'has user mentions' do
describe '#has_mentions?' do
context 'when no mentions' do
it 'returns false' do
expect(subject.mentioned_users_ids).to be nil
expect(subject.mentioned_projects_ids).to be nil
expect(subject.mentioned_groups_ids).to be nil
expect(subject.has_mentions?).to be false
end
end
context 'when mentioned_users_ids not null' do
subject { described_class.new(mentioned_users_ids: [1, 2, 3]) }
it 'returns true' do
expect(subject.has_mentions?).to be true
end
end
context 'when mentioned projects' do
subject { described_class.new(mentioned_projects_ids: [1, 2, 3]) }
it 'returns true' do
expect(subject.has_mentions?).to be true
end
end
context 'when mentioned groups' do
subject { described_class.new(mentioned_groups_ids: [1, 2, 3]) }
it 'returns true' do
expect(subject.has_mentions?).to be true
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