cache_markdown_field_spec.rb 8.8 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 26

    include CacheMarkdownField
    cache_markdown_field :foo
    cache_markdown_field :baz, pipeline: :single_line

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

36 37 38 39
    add_attr :cached_markdown_version

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

    def initialize(*)
      super

      # Pretend new is load
      clear_changes_information
    end

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

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

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

69 70
  let(:markdown) { '`Foo`' }
  let(:html) { '<p dir="auto"><code>Foo</code></p>' }
71

72 73
  let(:updated_markdown) { '`Bar`' }
  let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
74

75
  let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
76

77 78
  describe '.attributes' do
    it 'excludes cache attributes' do
79 80 81 82 83 84 85 86
      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
87
    end
88 89 90 91 92

    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(CacheMarkdownField::CACHE_VERSION) }
93 94
  end

95
  context 'a changed markdown field' do
96
    before do
97 98
      thing.foo = updated_markdown
      thing.save
99 100
    end

101 102
    it { expect(thing.foo_html).to eq(updated_html) }
    it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
103 104
  end

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  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

125
  context 'a non-markdown field changed' do
126
    before do
127 128
      thing.bar = 'OK'
      thing.save
129 130
    end

131 132 133 134
    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(CacheMarkdownField::CACHE_VERSION) }
135 136
  end

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

140
    before do
141
      thing.save
142 143
    end

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    it { expect(thing.foo_html).to eq(updated_html) }
    it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
  end

  describe '#cached_html_up_to_date?' do
    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 version is too early' do
      thing.cached_markdown_version -= 1

      is_expected.to be_falsy
    end

    it 'returns false when the version is too late' do
      thing.cached_markdown_version += 1

      is_expected.to be_falsy
    end

    it 'returns true when the version is just right' do
      thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION

      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
193 194 195 196 197 198

    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
199 200
  end

201
  describe '#refresh_markdown_cache' do
202 203 204 205
    before do
      thing.foo = updated_markdown
    end

206 207
    it 'fills all html fields' do
      thing.refresh_markdown_cache
208

209 210 211 212
      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
213

214 215
    it 'does not save the result' do
      expect(thing).not_to receive(:update_columns)
216

217 218
      thing.refresh_markdown_cache
    end
219

220 221 222
    it 'updates the markdown cache version' do
      thing.cached_markdown_version = nil
      thing.refresh_markdown_cache
223

224 225 226 227 228 229 230
      expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
    end
  end

  describe '#refresh_markdown_cache!' do
    before do
      thing.foo = updated_markdown
231 232
    end

233 234
    it 'fills all html fields' do
      thing.refresh_markdown_cache!
235

236 237 238 239
      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
240

241 242 243
    it 'skips saving if not persisted' do
      expect(thing).to receive(:persisted?).and_return(false)
      expect(thing).not_to receive(:update_columns)
244

245 246
      thing.refresh_markdown_cache!
    end
247

248 249 250 251
    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" => CacheMarkdownField::CACHE_VERSION)
252

253
      thing.refresh_markdown_cache!
254
    end
255 256 257
  end

  describe '#banzai_render_context' do
258 259 260 261
    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)
262 263 264
      expect(context[:project]).to be_nil
    end

265 266
    it 'excludes author if the object lacks an author' do
      is_expected.not_to have_key(:author)
267 268
    end

269 270
    it 'raises if the context for an unrecognised field is requested' do
      expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
271 272
    end

273 274 275 276
    it 'includes the pipeline' do
      baz = thing.banzai_render_context(:baz)

      expect(baz[:pipeline]).to eq(:single_line)
277 278
    end

279 280 281 282
    it 'returns copies of the context template' do
      template = thing.cached_markdown_fields[:baz]
      copy = thing.banzai_render_context(:baz)

283 284 285
      expect(copy).not_to be(template)
    end

286 287
    context 'with a project' do
      let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project_value) }
288

289 290 291
      it 'sets the project in the context' do
        is_expected.to have_key(:project)
        expect(context[:project]).to eq(:project_value)
292 293
      end

294 295
      it 'invalidates the cache when project changes' do
        thing.project = :new_project
296 297
        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)

298
        thing.save
299

300 301 302
        expect(thing.foo_html).to eq(updated_html)
        expect(thing.baz_html).to eq(updated_html)
        expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
303 304 305
      end
    end

306 307
    context 'with an author' do
      let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
308

309 310 311
      it 'sets the author in the context' do
        is_expected.to have_key(:author)
        expect(context[:author]).to eq(:author_value)
312 313
      end

314 315
      it 'invalidates the cache when author changes' do
        thing.author = :new_author
316 317
        allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)

318
        thing.save
319

320 321 322
        expect(thing.foo_html).to eq(updated_html)
        expect(thing.baz_html).to eq(updated_html)
        expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
323 324 325 326
      end
    end
  end
end