gitlab-org--gitlab-foss/spec/models/concerns/cache_markdown_field_spec.rb
Jan Provaznik 433bcf9b04 Add local markdown version
Cached markdown version is composed both from global and local
markdown version. This allows admins to bump version locally when
needed (e.g. when external URL is changed).
2019-02-06 19:46:59 +01:00

376 lines
10 KiB
Ruby

require 'spec_helper'
describe CacheMarkdownField do
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
include ActiveModel::Model
include ActiveModel::Dirty
include ActiveModel::Serialization
class_attribute :attribute_names
self.attribute_names = []
def attributes
attribute_names.each_with_object({}) do |name, hsh|
hsh[name.to_s] = send(name)
end
end
extend ActiveModel::Callbacks
define_model_callbacks :create, :update
include CacheMarkdownField
cache_markdown_field :foo
cache_markdown_field :baz, pipeline: :single_line
def self.add_attr(name)
self.attribute_names += [name]
define_attribute_methods(name)
attr_reader(name)
define_method("#{name}=") do |value|
write_attribute(name, value)
end
end
add_attr :cached_markdown_version
[:foo, :foo_html, :bar, :baz, :baz_html].each do |name|
add_attr(name)
end
def initialize(*)
super
# Pretend new is load
clear_changes_information
end
def read_attribute(name)
instance_variable_get("@#{name}")
end
def write_attribute(name, value)
send("#{name}_will_change!") unless value == read_attribute(name)
instance_variable_set("@#{name}", value)
end
def save
run_callbacks :update do
changes_applied
end
end
def has_attribute?(attr_name)
attribute_names.include?(attr_name)
end
end
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
let(:markdown) { '`Foo`' }
let(:html) { '<p dir="auto"><code>Foo</code></p>' }
let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
before do
stub_commonmark_sourcepos_disabled
end
describe '.attributes' do
it 'excludes cache attributes' do
expect(thing.attributes.keys.sort).to eq(%w[bar baz foo])
end
end
context 'an unchanged markdown field' do
before do
thing.foo = thing.foo
thing.save
end
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
context 'a changed markdown field' do
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
before do
thing.foo = updated_markdown
thing.save
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
context 'when a markdown field is set repeatedly to an empty string' do
it do
expect(thing).to receive(:refresh_markdown_cache).once
thing.foo = ''
thing.save
thing.foo = ''
thing.save
end
end
context 'when a markdown field is set repeatedly to a string which renders as empty html' do
it do
expect(thing).to receive(:refresh_markdown_cache).once
thing.foo = '[//]: # (This is also a comment.)'
thing.save
thing.foo = '[//]: # (This is also a comment.)'
thing.save
end
end
context 'when a markdown field and html field are both changed' do
it do
expect(thing).not_to receive(:refresh_markdown_cache)
thing.foo = '_look over there!_'
thing.foo_html = '<em>look over there!</em>'
thing.save
end
end
context 'a non-markdown field changed' do
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
before do
thing.bar = 'OK'
thing.save
end
it { expect(thing.bar).to eq('OK') }
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
context 'version is out of date' do
let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }
before do
thing.save
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
describe '#cached_html_up_to_date?' do
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
subject { thing.cached_html_up_to_date?(:foo) }
it 'returns false when the version is absent' do
thing.cached_markdown_version = nil
is_expected.to be_falsy
end
it 'returns false when the cached version is too old' do
thing.cached_markdown_version = cache_version - 1
is_expected.to be_falsy
end
it 'returns false when the cached version is in future' do
thing.cached_markdown_version = cache_version + 1
is_expected.to be_falsy
end
it 'returns false when the local version was bumped' do
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
thing.cached_markdown_version = cache_version
is_expected.to be_falsy
end
it 'returns true when the local version is default' do
thing.cached_markdown_version = cache_version
is_expected.to be_truthy
end
it 'returns true when the cached version is just right' do
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
thing.cached_markdown_version = cache_version + 2
is_expected.to be_truthy
end
it 'returns false if markdown has been changed but html has not' do
thing.foo = updated_html
is_expected.to be_falsy
end
it 'returns true if markdown has not been changed but html has' do
thing.foo_html = updated_html
is_expected.to be_truthy
end
it 'returns true if markdown and html have both been changed' do
thing.foo = updated_markdown
thing.foo_html = updated_html
is_expected.to be_truthy
end
it 'returns false if the markdown field is set but the html is not' do
thing.foo_html = nil
is_expected.to be_falsy
end
end
describe '#latest_cached_markdown_version' do
subject { thing.latest_cached_markdown_version }
it 'returns default version' do
thing.cached_markdown_version = nil
is_expected.to eq(cache_version)
end
end
describe '#refresh_markdown_cache' do
before do
thing.foo = updated_markdown
end
it 'fills all html fields' do
thing.refresh_markdown_cache
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
it 'does not save the result' do
expect(thing).not_to receive(:update_columns)
thing.refresh_markdown_cache
end
it 'updates the markdown cache version' do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
expect(thing.cached_markdown_version).to eq(cache_version)
end
end
describe '#refresh_markdown_cache!' do
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do
thing.foo = updated_markdown
end
it 'fills all html fields' do
thing.refresh_markdown_cache!
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
it 'skips saving if not persisted' do
expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
thing.refresh_markdown_cache!
end
it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
thing.refresh_markdown_cache!
end
end
describe '#banzai_render_context' do
subject(:context) { thing.banzai_render_context(:foo) }
it 'sets project to nil if the object lacks a project' do
is_expected.to have_key(:project)
expect(context[:project]).to be_nil
end
it 'excludes author if the object lacks an author' do
is_expected.not_to have_key(:author)
end
it 'raises if the context for an unrecognised field is requested' do
expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
end
it 'includes the pipeline' do
baz = thing.banzai_render_context(:baz)
expect(baz[:pipeline]).to eq(:single_line)
end
it 'returns copies of the context template' do
template = thing.cached_markdown_fields[:baz]
copy = thing.banzai_render_context(:baz)
expect(copy).not_to be(template)
end
context 'with a project' do
let(:project) { create(:project, group: create(:group)) }
let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: project) }
it 'sets the project in the context' do
is_expected.to have_key(:project)
expect(context[:project]).to eq(project)
end
it 'invalidates the cache when project changes' do
thing.project = :new_project
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
thing.save
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(cache_version)
end
end
context 'with an author' do
let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
it 'sets the author in the context' do
is_expected.to have_key(:author)
expect(context[:author]).to eq(:author_value)
end
it 'invalidates the cache when author changes' do
thing.author = :new_author
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
thing.save
expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(cache_version)
end
end
end
end