347 lines
12 KiB
Ruby
347 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Specifications for behavior common to all Mentionable implementations.
|
|
# Requires a shared context containing:
|
|
# - subject { "the mentionable implementation" }
|
|
# - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " }
|
|
# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } }
|
|
|
|
shared_context 'mentionable context' do
|
|
let(:project) { subject.project }
|
|
let(:author) { subject.author }
|
|
|
|
let(:mentioned_issue) { create(:issue, project: project) }
|
|
let!(:mentioned_mr) { create(:merge_request, source_project: project) }
|
|
let(:mentioned_commit) { project.commit("HEAD~1") }
|
|
|
|
let(:ext_proj) { create(:project, :public, :repository) }
|
|
let(:ext_issue) { create(:issue, project: ext_proj) }
|
|
let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
|
|
let(:ext_commit) { ext_proj.commit("HEAD~2") }
|
|
|
|
# Override to add known commits to the repository stub.
|
|
let(:extra_commits) { [] }
|
|
|
|
# A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference
|
|
# to this string and place it in their +mentionable_text+.
|
|
let(:ref_string) do
|
|
<<-MSG.strip_heredoc
|
|
These references are new:
|
|
Issue: #{mentioned_issue.to_reference}
|
|
Merge: #{mentioned_mr.to_reference}
|
|
Commit: #{mentioned_commit.to_reference}
|
|
|
|
This reference is a repeat and should only be mentioned once:
|
|
Repeat: #{mentioned_issue.to_reference}
|
|
|
|
These references are cross-referenced:
|
|
Issue: #{ext_issue.to_reference(project)}
|
|
Merge: #{ext_mr.to_reference(project)}
|
|
Commit: #{ext_commit.to_reference(project)}
|
|
|
|
This is a self-reference and should not be mentioned at all:
|
|
Self: #{backref_text}
|
|
MSG
|
|
end
|
|
|
|
before do
|
|
# Wire the project's repository to return the mentioned commit, and +nil+
|
|
# for any unrecognized commits.
|
|
allow_any_instance_of(::Repository).to receive(:commit).and_call_original
|
|
allow_any_instance_of(::Repository).to receive(:commit).with(mentioned_commit.short_id).and_return(mentioned_commit)
|
|
extra_commits.each do |commit|
|
|
allow_any_instance_of(::Repository).to receive(:commit).with(commit.short_id).and_return(commit)
|
|
end
|
|
|
|
set_mentionable_text.call(ref_string)
|
|
|
|
project.add_developer(author)
|
|
end
|
|
end
|
|
|
|
shared_examples 'a mentionable' do
|
|
include_context 'mentionable context'
|
|
|
|
it 'generates a descriptive back-reference' do
|
|
expect(subject.gfm_reference).to eq(backref_text)
|
|
end
|
|
|
|
it "extracts references from its reference property" do
|
|
# De-duplicate and omit itself
|
|
refs = subject.referenced_mentionables
|
|
expect(refs.size).to eq(6)
|
|
expect(refs).to include(mentioned_issue)
|
|
expect(refs).to include(mentioned_mr)
|
|
expect(refs).to include(mentioned_commit)
|
|
expect(refs).to include(ext_issue)
|
|
expect(refs).to include(ext_mr)
|
|
expect(refs).to include(ext_commit)
|
|
end
|
|
|
|
context 'when there are cached markdown fields' do
|
|
before do
|
|
if subject.is_a?(CacheMarkdownField)
|
|
subject.refresh_markdown_cache
|
|
end
|
|
end
|
|
|
|
it 'sends in cached markdown fields when appropriate' do
|
|
if subject.is_a?(CacheMarkdownField)
|
|
expect_next_instance_of(Gitlab::ReferenceExtractor) do |ext|
|
|
attrs = subject.class.mentionable_attrs.collect(&:first) & subject.cached_markdown_fields.markdown_fields
|
|
attrs.each do |field|
|
|
expect(ext).to receive(:analyze).with(subject.send(field), hash_including(rendered: anything))
|
|
end
|
|
end
|
|
|
|
expect(subject).not_to receive(:refresh_markdown_cache)
|
|
expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original
|
|
|
|
subject.all_references(author)
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'creates cross-reference notes' do
|
|
mentioned_objects = [mentioned_issue, mentioned_mr, mentioned_commit,
|
|
ext_issue, ext_mr, ext_commit]
|
|
|
|
mentioned_objects.each do |referenced|
|
|
expect(SystemNoteService).to receive(:cross_reference)
|
|
.with(referenced, subject.local_reference, author)
|
|
end
|
|
|
|
subject.create_cross_references!
|
|
end
|
|
end
|
|
|
|
shared_examples 'an editable mentionable' do
|
|
include_context 'mentionable context'
|
|
|
|
it_behaves_like 'a mentionable'
|
|
|
|
let(:new_issues) do
|
|
[create(:issue, project: project), create(:issue, project: ext_proj)]
|
|
end
|
|
|
|
context 'when there are cached markdown fields' do
|
|
before do
|
|
if subject.is_a?(CacheMarkdownField)
|
|
subject.refresh_markdown_cache
|
|
end
|
|
end
|
|
|
|
it 'refreshes markdown cache if necessary' do
|
|
subject.save!
|
|
|
|
set_mentionable_text.call('This is a text')
|
|
|
|
if subject.is_a?(CacheMarkdownField)
|
|
expect_next_instance_of(Gitlab::ReferenceExtractor) do |ext|
|
|
subject.cached_markdown_fields.markdown_fields.each do |field|
|
|
expect(ext).to receive(:analyze).with(subject.send(field), hash_including(rendered: anything))
|
|
end
|
|
end
|
|
|
|
expect(subject).to receive(:refresh_markdown_cache)
|
|
expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original
|
|
|
|
subject.all_references(author)
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'creates new cross-reference notes when the mentionable text is edited' do
|
|
subject.save
|
|
subject.create_cross_references!
|
|
|
|
new_text = <<-MSG.strip_heredoc
|
|
These references already existed:
|
|
|
|
Issue: #{mentioned_issue.to_reference}
|
|
|
|
Commit: #{mentioned_commit.to_reference}
|
|
|
|
---
|
|
|
|
This cross-project reference already existed:
|
|
|
|
Issue: #{ext_issue.to_reference(project)}
|
|
|
|
---
|
|
|
|
These two references are introduced in an edit:
|
|
|
|
Issue: #{new_issues[0].to_reference}
|
|
|
|
Cross: #{new_issues[1].to_reference(project)}
|
|
MSG
|
|
|
|
# These three objects were already referenced, and should not receive new
|
|
# notes
|
|
[mentioned_issue, mentioned_commit, ext_issue].each do |oldref|
|
|
expect(SystemNoteService).not_to receive(:cross_reference)
|
|
.with(oldref, any_args)
|
|
end
|
|
|
|
# These two issues are new and should receive reference notes
|
|
# In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
|
|
new_issues.each do |newref|
|
|
expect(SystemNoteService).to receive(:cross_reference)
|
|
.with(newref, subject.local_reference, author)
|
|
end
|
|
|
|
set_mentionable_text.call(new_text)
|
|
subject.create_new_cross_references!(author)
|
|
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
|