cacheable_attributes_spec.rb 7.32 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7 8 9
require 'spec_helper'

describe CacheableAttributes do
  let(:minimal_test_class) do
    Class.new do
      include ActiveModel::Model
      extend ActiveModel::Callbacks
10
      include ActiveModel::AttributeMethods
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
      define_model_callbacks :commit
      include CacheableAttributes

      def self.name
        'TestClass'
      end

      def self.first
        @_first ||= new('foo' => 'a')
      end

      def self.last
        @_last ||= new('foo' => 'a', 'bar' => 'b')
      end

26 27 28 29
      def self.column_names
        %w[foo bar baz]
      end

30 31
      attr_accessor :attributes

32
      def initialize(attrs = {}, *)
33 34 35 36 37
        @attributes = attrs
      end
    end
  end

38 39 40 41
  before do
    stub_const("MinimalTestClass", minimal_test_class)
  end

42 43
  shared_context 'with defaults' do
    before do
44
      MinimalTestClass.define_singleton_method(:defaults) do
45 46 47 48 49
        { foo: 'a', bar: 'b', baz: 'c' }
      end
    end
  end

50 51 52 53 54 55 56 57 58 59 60 61
  describe '.expire', :use_clean_rails_memory_store_caching, :request_store do
    it 'wipes the cache' do
      obj = MinimalTestClass.new
      obj.cache!
      expect(MinimalTestClass.cached).not_to eq(nil)

      MinimalTestClass.expire

      expect(MinimalTestClass.cached).to eq(nil)
    end
  end

62 63
  describe '.current_without_cache' do
    it 'defaults to last' do
64
      expect(MinimalTestClass.current_without_cache).to eq(MinimalTestClass.last)
65 66
    end

67
    it 'can be overridden' do
68
      MinimalTestClass.define_singleton_method(:current_without_cache) do
69 70 71
        first
      end

72
      expect(MinimalTestClass.current_without_cache).to eq(MinimalTestClass.first)
73 74 75 76 77
    end
  end

  describe '.cache_key' do
    it 'excludes cache attributes' do
78
      expect(MinimalTestClass.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Rails.version}")
79 80 81 82 83
    end
  end

  describe '.defaults' do
    it 'defaults to {}' do
84
      expect(MinimalTestClass.defaults).to eq({})
85 86 87 88 89
    end

    context 'with defaults defined' do
      include_context 'with defaults'

90
      it 'can be overridden' do
91
        expect(MinimalTestClass.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
92 93 94 95 96 97 98 99 100
      end
    end
  end

  describe '.build_from_defaults' do
    include_context 'with defaults'

    context 'without any attributes given' do
      it 'intializes a new object with the defaults' do
101
        expect(MinimalTestClass.build_from_defaults.attributes).to eq(MinimalTestClass.defaults.stringify_keys)
102 103 104
      end
    end

105
    context 'with attributes given' do
106
      it 'intializes a new object with the given attributes merged into the defaults' do
107
        expect(MinimalTestClass.build_from_defaults(foo: 'd').attributes['foo']).to eq('d')
108 109
      end
    end
110 111 112 113 114 115 116 117 118 119 120 121 122

    describe 'edge cases on concrete implementations' do
      describe '.build_from_defaults' do
        context 'without any attributes given' do
          it 'intializes all attributes even if they are nil' do
            record = ApplicationSetting.build_from_defaults

            expect(record).not_to be_persisted
            expect(record.sign_in_text).to be_nil
          end
        end
      end
    end
123 124 125 126
  end

  describe '.current', :use_clean_rails_memory_store_caching do
    context 'redis unavailable' do
127
      before do
128 129
        allow(MinimalTestClass).to receive(:last).and_return(:last)
        expect(Rails.cache).to receive(:read).with(MinimalTestClass.cache_key).and_raise(Redis::BaseError)
130 131 132 133
      end

      context 'in production environment' do
        before do
134
          stub_rails_env('production')
135
        end
136

137 138 139
        it 'returns an uncached record and logs a warning' do
          expect(Rails.logger).to receive(:warn).with("Cached record for TestClass couldn't be loaded, falling back to uncached record: Redis::BaseError")

140
          expect(MinimalTestClass.current).to eq(:last)
141 142 143 144 145
        end
      end

      context 'in other environments' do
        before do
146
          stub_rails_env('development')
147 148 149 150 151
        end

        it 'returns an uncached record and logs a warning' do
          expect(Rails.logger).not_to receive(:warn)

152
          expect { MinimalTestClass.current }.to raise_error(Redis::BaseError)
153
        end
154 155 156 157 158 159
      end
    end

    context 'when a record is not yet present' do
      it 'does not cache nil object' do
        # when missing settings a nil object is returned, but not cached
160
        allow(ApplicationSetting).to receive(:current_without_cache).twice.and_return(nil)
161

162
        expect(ApplicationSetting.current).to be_nil
163
        expect(ApplicationSetting.cache_backend.exist?(ApplicationSetting.cache_key)).to be(false)
164 165
      end

166 167
      it 'caches non-nil object' do
        create(:application_setting)
168

169
        expect(ApplicationSetting.current).to eq(ApplicationSetting.last)
170
        expect(ApplicationSetting.cache_backend.exist?(ApplicationSetting.cache_key)).to be(true)
171 172

        # subsequent calls retrieve the record from the cache
173 174 175 176 177 178 179 180
        last_record = ApplicationSetting.last
        expect(ApplicationSetting).not_to receive(:current_without_cache)
        expect(ApplicationSetting.current.attributes).to eq(last_record.attributes)
      end
    end

    describe 'edge cases' do
      describe 'caching behavior', :use_clean_rails_memory_store_caching do
Brett Walker's avatar
Brett Walker committed
181 182 183 184
        before do
          stub_commonmark_sourcepos_disabled
        end

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
        it 'retrieves upload fields properly' do
          ar_record = create(:appearance, :with_logo)
          ar_record.cache!

          cache_record = Appearance.current

          expect(cache_record).to be_persisted
          expect(cache_record.logo).to be_an(AttachmentUploader)
          expect(cache_record.logo.url).to end_with('/dk.png')
        end

        it 'retrieves markdown fields properly' do
          ar_record = create(:appearance, description: '**Hello**')
          ar_record.cache!

          cache_record = Appearance.current

          expect(cache_record.description).to eq('**Hello**')
Brett Walker's avatar
Brett Walker committed
203
          expect(cache_record.description_html).to eq('<p dir="auto"><strong>Hello</strong></p>')
204
        end
205 206
      end
    end
207

208
    it 'uses RequestStore in addition to Thread memory cache', :request_store do
209 210 211
      # Warm up the cache
      create(:application_setting).cache!

212 213
      expect(ApplicationSetting.cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend)
      expect(ApplicationSetting.cache_backend).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
214 215 216

      2.times { ApplicationSetting.current }
    end
217 218 219 220 221
  end

  describe '.cached', :use_clean_rails_memory_store_caching do
    context 'when cache is cold' do
      it 'returns nil' do
222
        expect(MinimalTestClass.cached).to be_nil
223 224 225
      end
    end

226
    context 'when cached is warm' do
227
      before do
228 229
        # Warm up the cache
        create(:appearance).cache!
230 231
      end

232 233 234
      it 'retrieves the record from cache' do
        expect(ActiveRecord::QueryRecorder.new { Appearance.cached }.count).to eq(0)
        expect(Appearance.cached).to eq(Appearance.current_without_cache)
235 236 237 238 239
      end
    end
  end

  describe '#cache!', :use_clean_rails_memory_store_caching do
240
    let(:record) { create(:appearance) }
241 242

    it 'caches the attributes' do
243 244 245 246 247 248 249
      record.cache!

      expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
    end

    describe 'edge cases' do
      let(:record) { create(:appearance) }
250

251 252 253 254 255
      it 'caches the attributes' do
        record.cache!

        expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
      end
256 257 258
    end
  end
end