cache_markdown_field_spec.rb 10.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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
Nick Thomas's avatar
Nick Thomas committed
21
    define_model_callbacks :create, :update
22 23 24 25

    include CacheMarkdownField
    cache_markdown_field :foo
    cache_markdown_field :baz, pipeline: :single_line
26
    cache_markdown_field :zoo, whitelisted: true
27

28 29 30 31 32 33
    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)
34 35 36
      end
    end

37 38
    add_attr :cached_markdown_version

39
    [:foo, :foo_html, :bar, :baz, :baz_html, :zoo, :zoo_html].each do |name|
40
      add_attr(name)
41 42 43 44 45 46 47 48 49
    end

    def initialize(*)
      super

      # Pretend new is load
      clear_changes_information
    end

50 51 52 53 54 55 56 57 58
    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

59
    def save
Nick Thomas's avatar
Nick Thomas committed
60
      run_callbacks :update do
61 62 63
        changes_applied
      end
    end
Jan Provaznik's avatar
Jan Provaznik committed
64 65 66 67

    def has_attribute?(attr_name)
      attribute_names.include?(attr_name)
    end
68 69 70 71 72 73
  end

  def thing_subclass(new_attr)
    Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
  end

74
  let(:markdown) { '`Foo`' }
Brett Walker's avatar
Brett Walker committed
75
  let(:html)     { '<p dir="auto"><code>Foo</code></p>' }
76

77
  let(:updated_markdown) { '`Bar`' }
Brett Walker's avatar
Brett Walker committed
78
  let(:updated_html)     { '<p dir="auto"><code>Bar</code></p>' }
79

Jan Provaznik's avatar
Jan Provaznik committed
80 81
  let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
  let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
82

Brett Walker's avatar
Brett Walker committed
83 84 85 86
  before do
    stub_commonmark_sourcepos_disabled
  end

87
  describe '.attributes' do
88 89
    it 'excludes cache attributes that is blacklisted by default' do
      expect(thing.attributes.keys.sort).to eq(%w[bar baz cached_markdown_version foo zoo zoo_html])
90 91 92 93 94 95 96
    end
  end

  context 'an unchanged markdown field' do
    before do
      thing.foo = thing.foo
      thing.save
97
    end
98 99 100 101

    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 }
Jan Provaznik's avatar
Jan Provaznik committed
102
    it { expect(thing.cached_markdown_version).to eq(cache_version) }
103 104
  end

105
  context 'a changed markdown field' do
Jan Provaznik's avatar
Jan Provaznik committed
106
    let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
107

108 109 110
    before do
      thing.foo = updated_markdown
      thing.save
111 112
    end

113 114
    it { expect(thing.foo_html).to eq(updated_html) }
    it { expect(thing.cached_markdown_version).to eq(cache_version) }
115 116
  end

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  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

Brett Walker's avatar
Brett Walker committed
137
  context 'when a markdown field and html field are both changed' do
138 139 140 141 142 143 144 145
    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

146
  context 'a non-markdown field changed' do
Jan Provaznik's avatar
Jan Provaznik committed
147
    let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
148

149 150 151
    before do
      thing.bar = 'OK'
      thing.save
152 153
    end

154 155 156 157
    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) }
158 159
  end

160 161 162
  context 'version is out of date' do
    let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }

163
    before do
164
      thing.save
165 166
    end

167
    it { expect(thing.foo_html).to eq(updated_html) }
Jan Provaznik's avatar
Jan Provaznik committed
168
    it { expect(thing.cached_markdown_version).to eq(cache_version) }
169 170 171
  end

  describe '#cached_html_up_to_date?' do
172
    let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
173

174
    subject { thing.cached_html_up_to_date?(:foo) }
175

176 177
    it 'returns false when the version is absent' do
      thing.cached_markdown_version = nil
178

179 180
      is_expected.to be_falsy
    end
181

Jan Provaznik's avatar
Jan Provaznik committed
182 183
    it 'returns false when the cached version is too old' do
      thing.cached_markdown_version = cache_version - 1
184

185 186
      is_expected.to be_falsy
    end
187

Jan Provaznik's avatar
Jan Provaznik committed
188 189
    it 'returns false when the cached version is in future' do
      thing.cached_markdown_version = cache_version + 1
190

191 192
      is_expected.to be_falsy
    end
193

Jan Provaznik's avatar
Jan Provaznik committed
194 195
    it 'returns false when the local version was bumped' do
      allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
196
      thing.cached_markdown_version = cache_version
197

Jan Provaznik's avatar
Jan Provaznik committed
198 199 200 201 202 203 204 205 206 207 208 209 210
      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

211 212
      is_expected.to be_truthy
    end
213

214 215
    it 'returns false if markdown has been changed but html has not' do
      thing.foo = updated_html
216

217 218
      is_expected.to be_falsy
    end
219

220 221
    it 'returns true if markdown has not been changed but html has' do
      thing.foo_html = updated_html
222

223 224
      is_expected.to be_truthy
    end
225

226 227 228
    it 'returns true if markdown and html have both been changed' do
      thing.foo = updated_markdown
      thing.foo_html = updated_html
229

230 231
      is_expected.to be_truthy
    end
232

233 234
    it 'returns false if the markdown field is set but the html is not' do
      thing.foo_html = nil
235

236
      is_expected.to be_falsy
237
    end
238 239 240 241
  end

  describe '#latest_cached_markdown_version' do
    subject { thing.latest_cached_markdown_version }
242

Jan Provaznik's avatar
Jan Provaznik committed
243
    it 'returns default version' do
244
      thing.cached_markdown_version = nil
Jan Provaznik's avatar
Jan Provaznik committed
245
      is_expected.to eq(cache_version)
246
    end
247 248
  end

249
  describe '#refresh_markdown_cache' do
250 251 252 253
    before do
      thing.foo = updated_markdown
    end

254 255
    it 'fills all html fields' do
      thing.refresh_markdown_cache
256

257 258 259 260
      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
261

262 263
    it 'does not save the result' do
      expect(thing).not_to receive(:update_columns)
264

265 266
      thing.refresh_markdown_cache
    end
267

268 269 270
    it 'updates the markdown cache version' do
      thing.cached_markdown_version = nil
      thing.refresh_markdown_cache
271

Jan Provaznik's avatar
Jan Provaznik committed
272
      expect(thing.cached_markdown_version).to eq(cache_version)
273 274 275 276
    end
  end

  describe '#refresh_markdown_cache!' do
277
    let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
278

279 280 281
    before do
      thing.foo = updated_markdown
    end
282

283 284
    it 'fills all html fields' do
      thing.refresh_markdown_cache!
285

286 287 288 289
      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
290

291 292 293
    it 'skips saving if not persisted' do
      expect(thing).to receive(:persisted?).and_return(false)
      expect(thing).not_to receive(:update_columns)
294

295 296
      thing.refresh_markdown_cache!
    end
297

298 299 300
    it 'saves the changes using #update_columns' do
      expect(thing).to receive(:persisted?).and_return(true)
      expect(thing).to receive(:update_columns)
301 302 303 304 305 306
        .with(
          "foo_html" => updated_html,
          "baz_html" => "",
          "zoo_html" => "",
          "cached_markdown_version" => cache_version
        )
307

308
      thing.refresh_markdown_cache!
309
    end
310 311 312
  end

  describe '#banzai_render_context' do
313 314 315 316
    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)
317 318 319
      expect(context[:project]).to be_nil
    end

320 321
    it 'excludes author if the object lacks an author' do
      is_expected.not_to have_key(:author)
322 323
    end

324 325
    it 'raises if the context for an unrecognised field is requested' do
      expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
326 327
    end

328 329 330 331
    it 'includes the pipeline' do
      baz = thing.banzai_render_context(:baz)

      expect(baz[:pipeline]).to eq(:single_line)
332 333
    end

334 335 336 337
    it 'returns copies of the context template' do
      template = thing.cached_markdown_fields[:baz]
      copy = thing.banzai_render_context(:baz)

338 339 340
      expect(copy).not_to be(template)
    end

341
    context 'with a project' do
342 343
      let(:project) { create(:project, group: create(:group)) }
      let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: project) }
344

345 346
      it 'sets the project in the context' do
        is_expected.to have_key(:project)
347
        expect(context[:project]).to eq(project)
348 349
      end

350 351
      it 'invalidates the cache when project changes' do
        thing.project = :new_project
352 353
        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)

354
        thing.save
355

356 357
        expect(thing.foo_html).to eq(updated_html)
        expect(thing.baz_html).to eq(updated_html)
Jan Provaznik's avatar
Jan Provaznik committed
358
        expect(thing.cached_markdown_version).to eq(cache_version)
359 360 361
      end
    end

362 363
    context 'with an author' do
      let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
364

365 366 367
      it 'sets the author in the context' do
        is_expected.to have_key(:author)
        expect(context[:author]).to eq(:author_value)
368 369
      end

370 371
      it 'invalidates the cache when author changes' do
        thing.author = :new_author
372 373
        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)

374
        thing.save
375

376 377
        expect(thing.foo_html).to eq(updated_html)
        expect(thing.baz_html).to eq(updated_html)
Jan Provaznik's avatar
Jan Provaznik committed
378
        expect(thing.cached_markdown_version).to eq(cache_version)
379 380 381 382
      end
    end
  end
end