BigW Consortium Gitlab

Split Markdown rendering & reference gathering

This splits the Markdown rendering and reference extraction phases into two distinct code bases. The reference extraction phase no longer relies on the html-pipeline Gem (and any related code) and allows for extracting of references from multiple HTML nodes in a single pass. This means that if you want to extract user references from 200 comments you no longer need to run 200 times N number of queries, instead only a handful of queries may be needed.
parent 94d5416d
......@@ -91,8 +91,8 @@ class Projects::WikisController < Projects::ApplicationController
def markdown_preview
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
......
......@@ -197,8 +197,8 @@ class ProjectsController < Projects::ApplicationController
def markdown_preview
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text),
......
......@@ -18,10 +18,6 @@ module Banzai
@object_sym ||= object_name.to_sym
end
def self.data_reference
@data_reference ||= "data-#{object_name.dasherize}"
end
def self.object_class_title
@object_title ||= object_class.name.titleize
end
......@@ -45,10 +41,6 @@ module Banzai
end
end
def self.referenced_by(node)
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
end
def object_class
self.class.object_class
end
......
......@@ -4,6 +4,8 @@ module Banzai
#
# This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range
def self.object_class
CommitRange
end
......@@ -14,34 +16,18 @@ module Banzai
end
end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-commit-range")
range = find_object(project, id)
return unless range
{ commit_range: range }
end
def initialize(*args)
super
@commit_map = {}
end
def self.find_object(project, id)
def find_object(project, id)
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
end
def find_object(*args)
self.class.find_object(*args)
end
def url_for_object(range, project)
h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project,
......
......@@ -4,6 +4,8 @@ module Banzai
#
# This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit
def self.object_class
Commit
end
......@@ -14,28 +16,12 @@ module Banzai
end
end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-commit")
commit = find_object(project, id)
return unless commit
{ commit: commit }
end
def self.find_object(project, id)
def find_object(project, id)
if project && project.valid_repo?
project.commit(id)
end
end
def find_object(*args)
self.class.find_object(*args)
end
def url_for_object(commit, project)
h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
......
......@@ -4,6 +4,8 @@ module Banzai
# References are ignored if the project doesn't use an external issue
# tracker.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
# Public: Find `JIRA-123` issue references in text
#
# ExternalIssueReferenceFilter.references_in(text) do |match, issue|
......@@ -21,18 +23,6 @@ module Banzai
end
end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-external-issue")
external_issue = ExternalIssue.new(id, project)
return unless external_issue
{ external_issue: external_issue }
end
def call
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
......
......@@ -5,18 +5,12 @@ module Banzai
#
# This filter supports cross-project references.
class IssueReferenceFilter < AbstractReferenceFilter
self.reference_type = :issue
def self.object_class
Issue
end
def self.user_can_see_reference?(user, node, context)
# It is not possible to check access rights for external issue trackers
return true if context[:project].try(:external_issue_tracker)
issue = Issue.find(node.attr('data-issue')) rescue nil
Ability.abilities.allowed?(user, :read_issue, issue)
end
def find_object(project, id)
project.get_issue(id)
end
......
......@@ -2,6 +2,8 @@ module Banzai
module Filter
# HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label
def self.object_class
Label
end
......
......@@ -5,6 +5,8 @@ module Banzai
#
# This filter supports cross-project references.
class MergeRequestReferenceFilter < AbstractReferenceFilter
self.reference_type = :merge_request
def self.object_class
MergeRequest
end
......
......@@ -2,6 +2,8 @@ module Banzai
module Filter
# HTML filter that replaces milestone references with links.
class MilestoneReferenceFilter < AbstractReferenceFilter
self.reference_type = :milestone
def self.object_class
Milestone
end
......
......@@ -7,8 +7,11 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
Querying.css(doc, 'a.gfm').each do |node|
unless user_can_see_reference?(node)
nodes = Querying.css(doc, 'a.gfm[data-reference-type]')
visible = nodes_visible_to_user(nodes)
nodes.each do |node|
unless visible.include?(node)
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
text = node.attr('data-original') || node.text
......@@ -21,20 +24,30 @@ module Banzai
private
def user_can_see_reference?(node)
if node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = Banzai::Filter.const_get(reference_type)
def nodes_visible_to_user(nodes)
per_type = Hash.new { |h, k| h[k] = [] }
visible = Set.new
nodes.each do |node|
per_type[node.attr('data-reference-type')] << node
end
per_type.each do |type, nodes|
parser = Banzai::ReferenceParser[type].new(project, current_user)
reference_filter.user_can_see_reference?(current_user, node, context)
else
true
visible.merge(parser.nodes_visible_to_user(current_user, nodes))
end
visible
end
def current_user
context[:current_user]
end
def project
context[:project]
end
end
end
end
......@@ -8,24 +8,8 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-project')
project_id = node.attr('data-project').to_i
return true if project_id == context[:project].try(:id)
project = Project.find(project_id) rescue nil
Ability.abilities.allowed?(user, :read_project, project)
else
true
end
end
def self.user_can_reference?(user, node, context)
true
end
def self.referenced_by(node)
raise NotImplementedError, "#{self} does not implement #{__method__}"
class << self
attr_accessor :reference_type
end
# Returns a data attribute String to attach to a reference link
......@@ -43,7 +27,9 @@ module Banzai
#
# Returns a String
def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize
attributes = attributes.reject { |_, v| v.nil? }
attributes[:reference_type] = self.class.reference_type
attributes.delete(:original) if context[:no_original_data]
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
......
module Banzai
module Filter
# HTML filter that gathers all referenced records that the current user has
# permission to view.
#
# Expected to be run in its own post-processing pipeline.
#
class ReferenceGathererFilter < HTML::Pipeline::Filter
def initialize(*)
super
result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
end
def call
Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
load_lazy_references unless ReferenceExtractor.lazy?
doc
end
private
def gather_references(node)
return unless node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = Banzai::Filter.const_get(reference_type)
return if context[:reference_filter] && reference_filter != context[:reference_filter]
return if author && !reference_filter.user_can_reference?(author, node, context)
return unless reference_filter.user_can_see_reference?(current_user, node, context)
references = reference_filter.referenced_by(node)
return unless references
references.each do |type, values|
Array.wrap(values).each do |value|
result[:references][type] << value
end
end
end
def load_lazy_references
refs = result[:references]
refs.each do |type, values|
refs[type] = ReferenceExtractor.lazily(values)
end
end
def current_user
context[:current_user]
end
def author
context[:author]
end
end
end
end
......@@ -5,6 +5,8 @@ module Banzai
#
# This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet
def self.object_class
Snippet
end
......
......@@ -4,6 +4,8 @@ module Banzai
#
# A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter
self.reference_type = :user
# Public: Find `@user` user references in text
#
# UserReferenceFilter.references_in(text) do |match, username|
......@@ -21,43 +23,6 @@ module Banzai
end
end
def self.referenced_by(node)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
return unless group
{ user: group.users }
elsif node.has_attribute?('data-user')
{ user: LazyReference.new(User, node.attr('data-user')) }
elsif node.has_attribute?('data-project')
project = Project.find(node.attr('data-project')) rescue nil
return unless project
{ user: project.team.members.flatten }
end
end
def self.user_can_see_reference?(user, node, context)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
Ability.abilities.allowed?(user, :read_group, group)
else
super
end
end
def self.user_can_reference?(user, node, context)
# Only team members can reference `@all`
if node.has_attribute?('data-project')
project = Project.find(node.attr('data-project')) rescue nil
return false unless project
user && project.team.member?(user)
else
super
end
end
def call
return doc if project.nil?
......@@ -114,9 +79,12 @@ module Banzai
def link_to_all(link_text: nil)
project = context[:project]
author = context[:author]
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
data = data_attribute(project: project.id)
data = data_attribute(project: project.id, author: author.try(:id))
text = link_text || User.reference_prefix + 'all'
link_tag(url, data, text)
......
module Banzai
class LazyReference
def self.load(refs)
lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
ids = refs.flat_map(&:ids)
klass.where(id: ids)
end
values + lazy_values
end
attr_reader :klass, :ids
def initialize(klass, ids)
@klass = klass
@ids = Array.wrap(ids).map(&:to_i)
end
def load
self.klass.where(id: self.ids)
end
end
end
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
def self.filters
FilterArray[
Filter::ReferenceGathererFilter
]
end
end
end
end
module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
class << self
LAZY_KEY = :banzai_reference_extractor_lazy
def lazy?
Thread.current[LAZY_KEY]
end
def lazily(values = nil, &block)
return (values || block.call).uniq if lazy?
begin
Thread.current[LAZY_KEY] = true
values ||= block.call
Banzai::LazyReference.load(values.uniq).uniq
ensure
Thread.current[LAZY_KEY] = false
end
end
end
def initialize
@texts = []
end
......@@ -31,23 +9,21 @@ module Banzai
@texts << Renderer.render(text, context)
end
def references(type, context = {})
filter = Banzai::Filter["#{type}_reference"]
def references(type, project, current_user = nil)
processor = Banzai::ReferenceParser[type].
new(project, current_user)
processor.process(html_documents)
end
context.merge!(
pipeline: :reference_extraction,
private
# ReferenceGathererFilter
reference_filter: filter
)
def html_documents
# This ensures that we don't memoize anything until we have a number of
# text blobs to parse.
return [] if @texts.empty?
self.class.lazily do
@texts.flat_map do |html|
text_context = context.dup
result = Renderer.render_result(html, text_context)
result[:references][type]
end.uniq
end
@html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) }
end
end
end
module Banzai
module ReferenceParser
# Returns the reference parser class for the given type
#
# Example:
#
# Banzai::ReferenceParser['issue']
#
# This would return the `Banzai::ReferenceParser::IssueParser` class.
def self.[](name)
const_get("#{name.to_s.camelize}Parser")
end
end
end
module Banzai
module ReferenceParser
# Base class for reference parsing classes.
#
# Each parser should also specify its reference type by calling
# `self.reference_type = ...` in the body of the class. The value of this
# method should be a symbol such as `:issue` or `:merge_request`. For
# example:
#
# class IssueParser < BaseParser
# self.reference_type = :issue
# end
#
# The reference type is used to determine what nodes to pass to the
# `referenced_by` method.
#
# Parser classes should either implement the instance method
# `references_relation` or overwrite `referenced_by`. The
# `references_relation` method is supposed to return an
# ActiveRecord::Relation used as a base relation for retrieving the objects
# referenced in a set of HTML nodes.
#
# Each class can implement two additional methods:
#
# * `nodes_user_can_reference`: returns an Array of nodes the given user can
# refer to.
# * `nodes_visible_to_user`: returns an Array of nodes that are visible to
# the given user.
#
# You only need to overwrite these methods if you want to tweak who can see
# which references. For example, the IssueParser class defines its own
# `nodes_visible_to_user` method so it can ensure users can only see issues
# they have access to.
class BaseParser
class << self
attr_accessor :reference_type
end
# Returns the attribute name containing the value for every object to be
# parsed by the current parser.
#
# For example, for a parser class that returns "Animal" objects this
# attribute would be "data-animal".
def self.data_attribute
@data_attribute ||= "data-#{reference_type.to_s.dasherize}"
end
def initialize(project = nil, current_user = nil)
@project = project
@current_user = current_user
end
# Returns all the nodes containing references that the user can refer to.
def nodes_user_can_reference(user, nodes)
nodes
end
# Returns all the nodes that are visible to the given user.
def nodes_visible_to_user(user, nodes)
projects = lazy { projects_for_nodes(nodes) }
project_attr = 'data-project'
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
if project && project.id == node_id
true
else
can?(user, :read_project, projects[node_id])
end
else
true
end
end
end
# Returns an Array of objects referenced by any of the given HTML nodes.
def referenced_by(nodes)
ids = unique_attribute_values(nodes, self.class.data_attribute)
references_relation.where(id: ids)
end
# Returns the ActiveRecord::Relation to use for querying references in the
# DB.
def references_relation
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
# Returns a Hash containing attribute values per project ID.
#
# The returned Hash uses the following format:
#
# { project id => [value1, value2, ...] }
#
# nodes - An Array of HTML nodes to process.
# attribute - The name of the attribute (as a String) for which to gather
# values.
#
# Returns a Hash.
def gather_attributes_per_project(nodes, attribute)
per_project = Hash.new { |hash, key| hash[key] = Set.new }
nodes.each do |node|
project_id = node.attr('data-project').to_i
id = node.attr(attribute)
per_project[project_id] << id if id
end
per_project
end
# Returns a Hash containing objects for an attribute grouped per their
# IDs.
#
# The returned Hash uses the following format:
#
# { id value => row }
#
# nodes - An Array of HTML nodes to process.
#
# collection - The model or ActiveRecord relation to use for retrieving
# rows from the database.
#
# attribute - The name of the attribute containing the primary key values
# for every row.
#
# Returns a Hash.
def grouped_objects_for_nodes(nodes, collection, attribute)
return {} if nodes.empty?
ids = unique_attribute_values(nodes, attribute)
collection.where(id: ids).each_with_object({}) do |row, hash|
hash[row.id] = row
end
end
# Returns an Array containing all unique values of an attribute of the
# given nodes.
def unique_attribute_values(nodes, attribute)
values = Set.new
nodes.each do |node|
if node.has_attribute?(attribute)
values << node.attr(attribute)
end
end
values.to_a
end
# Processes the list of HTML documents and returns an Array containing all
# the references.
def process(documents)
type = self.class.reference_type
nodes = documents.flat_map do |document|
Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
end
gather_references(nodes)
end
# Gathers the references for the given HTML nodes.
def gather_references(nodes)
nodes = nodes_user_can_reference(current_user, nodes)
nodes = nodes_visible_to_user(current_user, nodes)
referenced_by(nodes)
end
# Returns a Hash containing the projects for a given list of HTML nodes.
#
# The returned Hash uses the following format:
#
# { project ID => project }
#
def projects_for_nodes(nodes)
@projects_for_nodes ||=
grouped_objects_for_nodes(nodes, Project, 'data-project')
end
def can?(user, permission, subject)
Ability.abilities.allowed?(user, permission, subject)
end
def find_projects_for_hash_keys(hash)
Project.where(id: hash.keys)
end
private
attr_reader :current_user, :project
def lazy(&block)
Gitlab::Lazy.new(&block)
end
end
end
end
module Banzai
module ReferenceParser
class CommitParser < BaseParser
self.reference_type = :commit
def referenced_by(nodes)
commit_ids = commit_ids_per_project(nodes)
projects = find_projects_for_hash_keys(commit_ids)
projects.flat_map do |project|
find_commits(project, commit_ids[project.id])
end
end
def commit_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
def find_commits(project, ids)
commits = []
return commits unless project.valid_repo?
ids.each do |id|
commit = project.commit(id)
commits << commit if commit
end
commits
end
end
end
end
module Banzai
module ReferenceParser
class CommitRangeParser < BaseParser
self.reference_type = :commit_range
def referenced_by(nodes)
range_ids = commit_range_ids_per_project(nodes)
projects = find_projects_for_hash_keys(range_ids)
projects.flat_map do |project|
find_ranges(project, range_ids[project.id])
end
end
def commit_range_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
def find_ranges(project, range_ids)
ranges = []
range_ids.each do |id|
range = find_object(project, id)
ranges << range if range
end
ranges
end
def find_object(project, id)
range = CommitRange.new(id, project)
range.valid_commits? ? range : nil
end
end
end
end
module Banzai
module ReferenceParser
class ExternalIssueParser < BaseParser
self.reference_type = :external_issue
def referenced_by(nodes)
issue_ids = issue_ids_per_project(nodes)
projects = find_projects_for_hash_keys(issue_ids)
issues = []
projects.each do |project|
issue_ids[project.id].each do |id|
issues << ExternalIssue.new(id, project)
end
end
issues
end
def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
end
end
end
module Banzai
module ReferenceParser
class IssueParser < BaseParser
self.reference_type = :issue
def nodes_visible_to_user(user, nodes)
# It is not possible to check access rights for external issue trackers
return nodes if project && project.external_issue_tracker
issues = issues_for_nodes(nodes)
nodes.select do |node|
issue = issue_for_node(issues, node)
issue ? can?(user, :read_issue, issue) : false
end
end
def referenced_by(nodes)
issues = issues_for_nodes(nodes)
nodes.map { |node| issue_for_node(issues, node) }.uniq
end
def issues_for_nodes(nodes)
@issues_for_nodes ||= grouped_objects_for_nodes(
nodes,
Issue.all.includes(:author, :assignee, :project),
self.class.data_attribute
)
end
private
def issue_for_node(issues, node)
issues[node.attr(self.class.data_attribute).to_i]
end
end
end
end
module Banzai
module ReferenceParser
class LabelParser < BaseParser
self.reference_type = :label
def references_relation
Label
end
end
end
end
module Banzai
module ReferenceParser
class MergeRequestParser < BaseParser
self.reference_type = :merge_request
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
end
end
end
end
module Banzai
module ReferenceParser
class MilestoneParser < BaseParser
self.reference_type = :milestone
def references_relation
Milestone
end
end
end
end
module Banzai
module ReferenceParser
class SnippetParser < BaseParser
self.reference_type = :snippet
def references_relation
Snippet
end
end
end
end
module Banzai
module ReferenceParser
class UserParser < BaseParser
self.reference_type = :user
def referenced_by(nodes)
group_ids = []
user_ids = []
project_ids = []
nodes.each do |node|
if node.has_attribute?('data-group')
group_ids << node.attr('data-group').to_i
elsif node.has_attribute?(self.class.data_attribute)
user_ids << node.attr(self.class.data_attribute).to_i
elsif node.has_attribute?('data-project')
project_ids << node.attr('data-project').to_i
end
end
find_users_for_groups(group_ids) | find_users(user_ids) |
find_users_for_projects(project_ids)
end
def nodes_visible_to_user(user, nodes)
group_attr = 'data-group'
groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) }
visible = []
remaining = []
nodes.each do |node|
if node.has_attribute?(group_attr)
node_group = groups[node.attr(group_attr).to_i]
if node_group &&
can?(user, :read_group, node_group)
visible << node
end
# Remaining nodes will be processed by the parent class'
# implementation of this method.
else
remaining << node
end
end
visible + super(current_user, remaining)
end
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
projects = lazy { projects_for_nodes(nodes) }
users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) }
nodes.select do |node|
project_id = node.attr(project_attr)
user_id = node.attr(author_attr)
if project && project_id && project.id == project_id.to_i
true
elsif project_id && user_id
project = projects[project_id.to_i]
user = users[user_id.to_i]
project && user ? project.team.member?(user) : false
else
true
end
end
end
def find_users(ids)
return [] if ids.empty?
User.where(id: ids).to_a
end
def find_users_for_groups(ids)
return [] if ids.empty?
User.joins(:group_members).where(members: { source_id: ids }).to_a
end
def find_users_for_projects(ids)
return [] if ids.empty?
Project.where(id: ids).flat_map { |p| p.team.members.to_a }
end
end
end
end
......@@ -4,10 +4,9 @@ module Gitlab
REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil, author = nil)
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
@author = author
@references = {}
......@@ -18,17 +17,21 @@ module Gitlab
super(text, context.merge(project: project))
end
def references(type)
super(type, project, current_user)
end
REFERABLES.each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, reference_context)
@references[type] ||= references(type)
end
end
def issues
if project && project.jira_tracker?
@references[:external_issue] ||= references(:external_issue, reference_context)
@references[:external_issue] ||= references(:external_issue)
else
@references[:issue] ||= references(:issue, reference_context)
@references[:issue] ||= references(:issue)
end
end
......@@ -46,11 +49,5 @@ module Gitlab
@pattern = Regexp.union(patterns.compact)
end
private
def reference_context
{ project: project, current_user: current_user, author: author }
end
end
end
......@@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end
context 'cross-project reference' do
......@@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end
context 'cross-project URL reference' do
......@@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end
end
......@@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end
context 'cross-project reference' do
......@@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
exp = act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end
context 'cross-project URL reference' do
......@@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end
end
......@@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>")
......@@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end
context 'cross-project URL reference' do
......@@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end
context 'cross-project reference in link href' do
......@@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end
context 'cross-project URL in link href' do
......@@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end
end
......@@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label]
end
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
......@@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to have_attribute('data-label')
expect(link.attr('data-label')).to eq label.id.to_s
end
it 'adds to the results hash' do
result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label]
end
end
describe 'cross project label references' do
......
......@@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end
context 'cross-project reference' do
......@@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end
context 'cross-project URL reference' do
......@@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
doc = reference_filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end
end
......@@ -48,11 +48,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
namespace_project_milestone_path(project.namespace, project, milestone)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Milestone #{reference}")
expect(result[:references][:milestone]).to eq [milestone]
end
context 'Integer-based references' do
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
......@@ -151,11 +146,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
expect(link).to have_attribute('data-milestone')
expect(link.attr('data-milestone')).to eq milestone.id.to_s
end
it 'adds to the results hash' do
result = reference_pipeline_result("Milestone #{reference}")
expect(result[:references][:milestone]).to eq [milestone]
end
end
describe 'cross project milestone references' do
......
......@@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
context 'with data-project' do
let(:parser_class) do
Class.new(Banzai::ReferenceParser::BaseParser) do
self.reference_type = :test
end
end
before do
allow(Banzai::ReferenceParser).to receive(:[]).
with('test').
and_return(parser_class)
end
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
......@@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project)
project.team << [user, :master]
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Project references' do
link = reference_link(project: 12345, reference_filter: 'ReferenceFilter')
link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error
end
......@@ -51,7 +63,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: non_member)
expect(doc.css('a').length).to eq 0
......@@ -62,7 +74,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, author: author)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: author)
expect(doc.css('a').length).to eq 1
......@@ -73,7 +85,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, assignee: assignee)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: assignee)
expect(doc.css('a').length).to eq 1
......@@ -85,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project.team << [member, :developer]
issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: member)
expect(doc.css('a').length).to eq 1
......@@ -96,7 +108,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: admin)
expect(doc.css('a').length).to eq 1
......@@ -108,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
......@@ -121,7 +133,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
user = create(:user)
group = create(:group, :private)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
......@@ -132,14 +144,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
group = create(:group, :private)
group.add_developer(user)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Group references' do
link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
link = reference_link(group: 12345, reference_type: 'user')
expect { filter(link) }.not_to raise_error
end
......@@ -149,7 +161,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows any User reference' do
user = create(:user)
link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
link = reference_link(user: user.id, reference_type: 'user')
doc = filter(link)
expect(doc.css('a').length).to eq 1
......
require 'spec_helper'
describe Banzai::Filter::ReferenceGathererFilter, lib: true do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper
def reference_link(data)
link_to('text', '', class: 'gfm', data: data)
end
context "for issue references" do
context 'with data-project' do
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
issue = create(:issue, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
result = pipeline_result(link, current_user: user)
expect(result[:references][:issue]).to be_empty
end
it 'allows permitted Project references' do
user = create(:user)
project = create(:empty_project)
issue = create(:issue, project: project)
project.team << [user, :master]
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
result = pipeline_result(link, current_user: user)
expect(result[:references][:issue]).to eq([issue])
end
it 'handles invalid Project references' do
link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter')
expect { pipeline_result(link) }.not_to raise_error
end
end
end
context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
group = create(:group)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
result = pipeline_result(link, current_user: user)
expect(result[:references][:user]).to be_empty
end
it 'allows permitted Group references' do
user = create(:user)
group = create(:group)
group.add_developer(user)
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
result = pipeline_result(link, current_user: user)
expect(result[:references][:user]).to eq([user])
end
it 'handles invalid Group references' do
link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
expect { pipeline_result(link) }.not_to raise_error
end
end
context 'with data-user' do
it 'allows any User reference' do
user = create(:user)
link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
result = pipeline_result(link)
expect(result[:references][:user]).to eq([user])
end
end
end
end
......@@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end
context 'cross-project reference' do
......@@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end
context 'cross-project URL reference' do
......@@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
it 'adds to the results hash' do
result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end
end
......@@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
it 'supports a special @all mention' do
doc = reference_filter("Hey #{reference}")
doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
context "when the author is a member of the project" do
it 'includes a data-author attribute when there is an author' do
doc = reference_filter(reference, author: user)
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}", author: project.creator)
expect(result[:references][:user]).to eq [project.creator]
end
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
end
context "when the author is not a member of the project" do
let(:other_user) { create(:user) }
it 'does not include a data-author attribute when there is no author' do
doc = reference_filter(reference)
it "doesn't add to the results hash" do
result = reference_pipeline_result("Hey #{reference}", author: other_user)
expect(result[:references][:user]).to eq []
end
expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end
end
......@@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end
context 'mentioning a group' do
......@@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-group')
expect(link.attr('data-group')).to eq group.id.to_s
end
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq group.users
end
end
it 'links with adjacent text' do
......@@ -151,10 +135,5 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
it 'adds to the results hash' do
result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::BaseParser, lib: true do
include ReferenceParserHelpers
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
subject do
klass = Class.new(described_class) do
self.reference_type = :foo
end
klass.new(project, user)
end
describe '.reference_type=' do
it 'sets the reference type' do
dummy = Class.new(described_class)
dummy.reference_type = :foo
expect(dummy.reference_type).to eq(:foo)
end
end
describe '#nodes_visible_to_user' do
let(:link) { empty_html_link }
context 'when the link has a data-project attribute' do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns the nodes if the user can read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the attribute value is empty' do
link['data-project'] = ''
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
it 'returns an empty Array when the user can not read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-project attribute' do
it 'returns the nodes' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
describe '#nodes_user_can_reference' do
it 'returns the nodes' do
link = double(:link)
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
end
describe '#referenced_by' do
context 'when references_relation is implemented' do
it 'returns a collection of objects' do
links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>").
children
expect(subject).to receive(:references_relation).and_return(User)
expect(subject.referenced_by(links)).to eq([user])
end
end
context 'when references_relation is not implemented' do
it 'raises NotImplementedError' do
links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children
expect { subject.referenced_by(links) }.
to raise_error(NotImplementedError)
end
end
end
describe '#references_relation' do
it 'raises NotImplementedError' do
expect { subject.references_relation }.to raise_error(NotImplementedError)
end
end
describe '#gather_attributes_per_project' do
it 'returns a Hash containing attribute values per project' do
link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>').
children[0]
hash = subject.gather_attributes_per_project([link], 'data-foo')
expect(hash).to be_an_instance_of(Hash)
expect(hash[1].to_a).to eq(['2'])
end
end
describe '#grouped_objects_for_nodes' do
it 'returns a Hash grouping objects per ID' do
nodes = [double(:node)]
expect(subject).to receive(:unique_attribute_values).
with(nodes, 'data-user').
and_return([user.id])
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
expect(hash).to eq({ user.id => user })
end
it 'returns an empty Hash when the list of nodes is empty' do
expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
end
end
describe '#unique_attribute_values' do
it 'returns an Array of unique values' do
link = double(:link)
expect(link).to receive(:has_attribute?).
with('data-foo').
twice.
and_return(true)
expect(link).to receive(:attr).
with('data-foo').
twice.
and_return('1')
nodes = [link, link]
expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1'])
end
end
describe '#process' do
it 'gathers the references for every node matching the reference type' do
dummy = Class.new(described_class) do
self.reference_type = :test
end
instance = dummy.new(project, user)
document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
expect(instance).to receive(:gather_references).
with([document.children[1]]).
and_return([user])
expect(instance.process([document])).to eq([user])
end
end
describe '#gather_references' do
let(:link) { double(:link) }
it 'does not process links a user can not reference' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([])
expect(subject).to receive(:referenced_by).with([])
subject.gather_references([link])
end
it 'does not process links a user can not see' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([link])
expect(subject).to receive(:nodes_visible_to_user).
with(user, [link]).
and_return([])
expect(subject).to receive(:referenced_by).with([])
subject.gather_references([link])
end
it 'returns the references if a user can reference and see a link' do
expect(subject).to receive(:nodes_user_can_reference).
with(user, [link]).
and_return([link])
expect(subject).to receive(:nodes_visible_to_user).
with(user, [link]).
and_return([link])
expect(subject).to receive(:referenced_by).with([link])
subject.gather_references([link])
end
end
describe '#can?' do
it 'delegates the permissions check to the Ability class' do
user = double(:user)
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, project)
subject.can?(user, :read_project, project)
end
end
describe '#find_projects_for_hash_keys' do
it 'returns a list of Projects' do
expect(subject.find_projects_for_hash_keys(project.id => project)).
to eq([project])
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::CommitParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link has a data-commit attribute' do
before do
link['data-commit'] = '123'
end
it 'returns an Array of commits' do
commit = double(:commit)
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject).to receive(:find_commits).
with(project, ['123']).
and_return([commit])
expect(subject.referenced_by([link])).to eq([commit])
end
it 'returns an empty Array when the commit could not be found' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject).to receive(:find_commits).
with(project, ['123']).
and_return([])
expect(subject.referenced_by([link])).to eq([])
end
it 'skips projects without valid repositories' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(false)
expect(subject.referenced_by([link])).to eq([])
end
end
context 'when the link does not have a data-commit attribute' do
it 'returns an empty Array' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
allow_any_instance_of(Project).to receive(:valid_repo?).
and_return(true)
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#commit_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing commit IDs per project' do
link['data-commit'] = '123'
hash = subject.commit_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123'])
end
it 'does not add a project when the data-commit attribute is empty' do
hash = subject.commit_ids_per_project([link])
expect(hash).to be_empty
end
end
describe '#find_commits' do
it 'returns an Array of commit objects' do
commit = double(:commit)
expect(project).to receive(:commit).with('123').and_return(commit)
expect(project).to receive(:valid_repo?).and_return(true)
expect(subject.find_commits(project, %w{123})).to eq([commit])
end
it 'skips commit IDs for which no commit could be found' do
expect(project).to receive(:commit).with('123').and_return(nil)
expect(project).to receive(:valid_repo?).and_return(true)
expect(subject.find_commits(project, %w{123})).to eq([])
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link as a data-commit-range attribute' do
before do
link['data-commit-range'] = '123..456'
end
it 'returns an Array of commit ranges' do
range = double(:range)
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(range)
expect(subject.referenced_by([link])).to eq([range])
end
it 'returns an empty Array when the commit range could not be found' do
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(nil)
expect(subject.referenced_by([link])).to eq([])
end
end
context 'when the link does not have a data-commit-range attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#commit_range_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing range IDs per project' do
link['data-commit-range'] = '123..456'
hash = subject.commit_range_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123..456'])
end
it 'does not add a project when the data-commit-range attribute is empty' do
hash = subject.commit_range_ids_per_project([link])
expect(hash).to be_empty
end
end
describe '#find_ranges' do
it 'returns an Array of range objects' do
range = double(:commit)
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(range)
expect(subject.find_ranges(project, ['123..456'])).to eq([range])
end
it 'skips ranges that could not be found' do
expect(subject).to receive(:find_object).
with(project, '123..456').
and_return(nil)
expect(subject.find_ranges(project, ['123..456'])).to eq([])
end
end
describe '#find_object' do
let(:range) { double(:range) }
before do
expect(CommitRange).to receive(:new).and_return(range)
end
context 'when the range has valid commits' do
it 'returns the commit range' do
expect(range).to receive(:valid_commits?).and_return(true)
expect(subject.find_object(project, '123..456')).to eq(range)
end
end
context 'when the range does not have any valid commits' do
it 'returns nil' do
expect(range).to receive(:valid_commits?).and_return(false)
expect(subject.find_object(project, '123..456')).to be_nil
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
link['data-project'] = project.id.to_s
end
context 'when the link has a data-external-issue attribute' do
it 'returns an Array of ExternalIssue instances' do
link['data-external-issue'] = '123'
refs = subject.referenced_by([link])
expect(refs).to eq([ExternalIssue.new('123', project)])
end
end
context 'when the link does not have a data-external-issue attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link does not have a data-project attribute' do
it 'returns an empty Array' do
expect(subject.referenced_by([link])).to eq([])
end
end
end
describe '#issue_ids_per_project' do
before do
link['data-project'] = project.id.to_s
end
it 'returns a Hash containing range IDs per project' do
link['data-external-issue'] = '123'
hash = subject.issue_ids_per_project([link])
expect(hash).to be_an_instance_of(Hash)
expect(hash[project.id].to_a).to eq(['123'])
end
it 'does not add a project when the data-external-issue attribute is empty' do
hash = subject.issue_ids_per_project([link])
expect(hash).to be_empty
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::IssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before do
link['data-issue'] = issue.id.to_s
end
it 'returns the nodes when the user can read the issue' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_issue, issue).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the user can not read the issue' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_issue, issue).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-issue attribute' do
it 'returns an empty Array' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the project uses an external issue tracker' do
it 'returns all nodes' do
link = double(:link)
expect(project).to receive(:external_issue_tracker).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
describe '#referenced_by' do
context 'when the link has a data-issue attribute' do
context 'using an existing issue ID' do
before do
link['data-issue'] = issue.id.to_s
end
it 'returns an Array of issues' do
expect(subject.referenced_by([link])).to eq([issue])
end
it 'returns an empty Array when the list of nodes is empty' do
expect(subject.referenced_by([link])).to eq([issue])
expect(subject.referenced_by([])).to eq([])
end
end
end
end
describe '#issues_for_nodes' do
it 'returns a Hash containing the issues for a list of nodes' do
link['data-issue'] = issue.id.to_s
nodes = [link]
expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue })
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::LabelParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-label attribute' do
context 'using an existing label ID' do
it 'returns an Array of labels' do
link['data-label'] = label.id.to_s
expect(subject.referenced_by([link])).to eq([label])
end
end
context 'using a non-existing label ID' do
it 'returns an empty Array' do
link['data-label'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
include ReferenceParserHelpers
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
subject { described_class.new(merge_request.target_project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
it 'returns an Array of merge requests' do
link['data-merge-request'] = merge_request.id.to_s
expect(subject.referenced_by([link])).to eq([merge_request])
end
end
context 'using a non-existing merge request ID' do
it 'returns an empty Array' do
link['data-merge-request'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::MilestoneParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-milestone attribute' do
context 'using an existing milestone ID' do
it 'returns an Array of milestones' do
link['data-milestone'] = milestone.id.to_s
expect(subject.referenced_by([link])).to eq([milestone])
end
end
context 'using a non-existing milestone ID' do
it 'returns an empty Array' do
link['data-milestone'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::SnippetParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:snippet) { create(:snippet, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
it 'returns an Array of snippets' do
link['data-snippet'] = snippet.id.to_s
expect(subject.referenced_by([link])).to eq([snippet])
end
end
context 'using a non-existing snippet ID' do
it 'returns an empty Array' do
link['data-snippet'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
require 'spec_helper'
describe Banzai::ReferenceParser::UserParser, lib: true do
include ReferenceParserHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public, group: group, creator: user) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#referenced_by' do
context 'when the link has a data-group attribute' do
context 'using an existing group ID' do
before do
link['data-group'] = project.group.id.to_s
end
it 'returns the users of the group' do
create(:group_member, group: group, user: user)
expect(subject.referenced_by([link])).to eq([user])
end
it 'returns an empty Array when the group has no users' do
expect(subject.referenced_by([link])).to eq([])
end
end
context 'using a non-existing group ID' do
it 'returns an empty Array' do
link['data-group'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
context 'when the link has a data-user attribute' do
it 'returns an Array of users' do
link['data-user'] = user.id.to_s
expect(subject.referenced_by([link])).to eq([user])
end
end
context 'when the link has a data-project attribute' do
context 'using an existing project ID' do
let(:contributor) { create(:user) }
before do
project.team << [user, :developer]
project.team << [contributor, :developer]
end
it 'returns the members of a project' do
link['data-project'] = project.id.to_s
# This uses an explicit sort to make sure this spec doesn't randomly
# fail when objects are returned in a different order.
refs = subject.referenced_by([link]).sort_by(&:id)
expect(refs).to eq([user, contributor])
end
end
context 'using a non-existing project ID' do
it 'returns an empty Array' do
link['data-project'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
describe '#nodes_visible_to_use?' do
context 'when the link has a data-group attribute' do
context 'using an existing group ID' do
before do
link['data-group'] = group.id.to_s
end
it 'returns the nodes if the user can read the group' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_group, group).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array if the user can not read the group' do
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_group, group).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'when the link does not have a data-group attribute' do
context 'with a data-project attribute' do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
expect(Ability.abilities).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns the nodes if the user can read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array if the user can not read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability.abilities).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context 'without a data-project attribute' do
it 'returns the nodes' do
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end
end
end
describe '#nodes_user_can_reference' do
context 'when the link has a data-author attribute' do
it 'returns the nodes when the user is a member of the project' do
other_project = create(:project)
other_project.team << [user, :developer]
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
it 'returns an empty Array when the project could not be found' do
link['data-project'] = ''
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
it 'returns an empty Array when the user could not be found' do
other_project = create(:project)
link['data-project'] = other_project.id.to_s
link['data-author'] = ''
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
it 'returns an empty Array when the user is not a team member' do
other_project = create(:project)
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
end
end
context 'when the link does not have a data-author attribute' do
it 'returns the nodes' do
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
end
end
end
end
......@@ -40,8 +40,7 @@ module FilterSpecHelper
filters = [
Banzai::Filter::AutolinkFilter,
described_class,
Banzai::Filter::ReferenceGathererFilter
described_class
]
HTML::Pipeline.new(filters, context)
......
module ReferenceParserHelpers
def empty_html_link
Nokogiri::HTML.fragment('<a></a>').children[0]
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