daad7144ec
This refactors the Markdown pipeline so it supports the rendering of multiple documents that may belong to different projects. An example of where this happens is when displaying the event feed of a group. In this case we retrieve events for all projects in the group. Previously we would group events per project and render these chunks separately, but this would result in many SQL queries being executed. By extending the Markdown pipeline to support this out of the box we can drastically reduce the number of SQL queries. To achieve this we introduce a new object to the pipeline: Banzai::RenderContext. This object simply wraps two other objects: an optional Project instance, and an optional User instance. On its own this wouldn't be very helpful, but a RenderContext can also be used to associate HTML documents with specific Project instances. This work is done in Banzai::ObjectRenderer and allows us to reuse as many queries (and results) as possible.
326 lines
8.9 KiB
Ruby
326 lines
8.9 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe Banzai::ReferenceParser::BaseParser do
|
|
include ReferenceParserHelpers
|
|
|
|
let(:user) { create(:user) }
|
|
let(:project) { create(:project, :public) }
|
|
let(:context) { Banzai::RenderContext.new(project, user) }
|
|
|
|
subject do
|
|
klass = Class.new(described_class) do
|
|
self.reference_type = :foo
|
|
end
|
|
|
|
klass.new(context)
|
|
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 '#project_for_node' do
|
|
it 'returns the Project for a node' do
|
|
document = instance_double('document', fragment?: false)
|
|
project = instance_double('project')
|
|
object = instance_double('object', project: project)
|
|
node = instance_double('node', document: document)
|
|
|
|
context.associate_document(document, object)
|
|
|
|
expect(subject.project_for_node(node)).to eq(project)
|
|
end
|
|
end
|
|
|
|
describe '#nodes_visible_to_user' do
|
|
let(:link) { empty_html_link }
|
|
|
|
context 'when the link has a data-project attribute' do
|
|
it 'checks if user can read the resource' do
|
|
link['data-project'] = project.id.to_s
|
|
|
|
expect(subject).to receive(:can_read_reference?).with(user, project, link)
|
|
|
|
subject.nodes_visible_to_user(user, [link])
|
|
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 node' do
|
|
link = double(:link)
|
|
|
|
expect(link).to receive(:has_attribute?)
|
|
.with('data-user')
|
|
.and_return(true)
|
|
|
|
expect(link).to receive(:attr)
|
|
.with('data-user')
|
|
.and_return(user.id.to_s)
|
|
|
|
nodes = [link]
|
|
|
|
expect(subject).to receive(:unique_attribute_values)
|
|
.with(nodes, 'data-user')
|
|
.and_return([user.id.to_s])
|
|
|
|
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
|
|
|
|
expect(hash).to eq({ link => user })
|
|
end
|
|
|
|
it 'returns an empty Hash when entry does not exist in the database', :request_store do
|
|
link = double(:link)
|
|
|
|
expect(link).to receive(:has_attribute?)
|
|
.with('data-user')
|
|
.and_return(true)
|
|
|
|
expect(link).to receive(:attr)
|
|
.with('data-user')
|
|
.and_return('1')
|
|
|
|
nodes = [link]
|
|
bad_id = user.id + 100
|
|
|
|
expect(subject).to receive(:unique_attribute_values)
|
|
.with(nodes, 'data-user')
|
|
.and_return([bad_id.to_s])
|
|
|
|
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
|
|
|
|
expect(hash).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(Banzai::RenderContext.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).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
|
|
|
|
describe '#collection_objects_for_ids' do
|
|
context 'with RequestStore disabled' do
|
|
it 'queries the collection directly' do
|
|
collection = User.all
|
|
|
|
expect(collection).to receive(:where).twice.and_call_original
|
|
|
|
2.times do
|
|
expect(subject.collection_objects_for_ids(collection, [user.id]))
|
|
.to eq([user])
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with RequestStore enabled' do
|
|
before do
|
|
cache = Hash.new { |hash, key| hash[key] = {} }
|
|
|
|
allow(RequestStore).to receive(:active?).and_return(true)
|
|
allow(subject).to receive(:collection_cache).and_return(cache)
|
|
end
|
|
|
|
it 'queries the collection on the first call' do
|
|
expect(subject.collection_objects_for_ids(User, [user.id]))
|
|
.to eq([user])
|
|
end
|
|
|
|
it 'does not query previously queried objects' do
|
|
collection = User.all
|
|
|
|
expect(collection).to receive(:where).once.and_call_original
|
|
|
|
2.times do
|
|
expect(subject.collection_objects_for_ids(collection, [user.id]))
|
|
.to eq([user])
|
|
end
|
|
end
|
|
|
|
it 'casts String based IDs to Fixnums before querying objects' do
|
|
2.times do
|
|
expect(subject.collection_objects_for_ids(User, [user.id.to_s]))
|
|
.to eq([user])
|
|
end
|
|
end
|
|
|
|
it 'queries any additional objects after the first call' do
|
|
other_user = create(:user)
|
|
|
|
expect(subject.collection_objects_for_ids(User, [user.id]))
|
|
.to eq([user])
|
|
|
|
expect(subject.collection_objects_for_ids(User, [user.id, other_user.id]))
|
|
.to eq([user, other_user])
|
|
end
|
|
|
|
it 'caches objects on a per collection class basis' do
|
|
expect(subject.collection_objects_for_ids(User, [user.id]))
|
|
.to eq([user])
|
|
|
|
expect(subject.collection_objects_for_ids(Project, [project.id]))
|
|
.to eq([project])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#collection_cache_key' do
|
|
it 'returns the cache key for a Class' do
|
|
expect(subject.collection_cache_key(Project)).to eq(Project)
|
|
end
|
|
|
|
it 'returns the cache key for an ActiveRecord::Relation' do
|
|
expect(subject.collection_cache_key(Project.all)).to eq(Project)
|
|
end
|
|
end
|
|
end
|