aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/test/controller/caching_test.rb
blob: aae7d0272fed310be4c6fda2b1ccb7a8819825c0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# frozen_string_literal: true
require "fileutils"
require "abstract_unit"
require "lib/controller/fake_models"

CACHE_DIR = "test_cache"
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR)

class FragmentCachingMetalTestController < ActionController::Metal
  abstract!

  include ActionController::Caching

  def some_action; end
end

class FragmentCachingMetalTest < ActionController::TestCase
  def setup
    super
    @store = ActiveSupport::Cache::MemoryStore.new
    @controller = FragmentCachingMetalTestController.new
    @controller.perform_caching = true
    @controller.cache_store = @store
    @params = { controller: "posts", action: "index" }
    @controller.params = @params
    @controller.request = @request
    @controller.response = @response
  end
end

class CachingController < ActionController::Base
  abstract!

  self.cache_store = :file_store, FILE_STORE_PATH
end

class FragmentCachingTestController < CachingController
  def some_action; end
end

class FragmentCachingTest < ActionController::TestCase
  ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)

  def setup
    super
    @store = ActiveSupport::Cache::MemoryStore.new
    @controller = FragmentCachingTestController.new
    @controller.perform_caching = true
    @controller.cache_store = @store
    @params = { controller: "posts", action: "index" }
    @controller.params = @params
    @controller.request = @request
    @controller.response = @response

    @m1v1 = ModelWithKeyAndVersion.new("model/1", "1")
    @m1v2 = ModelWithKeyAndVersion.new("model/1", "2")
    @m2v1 = ModelWithKeyAndVersion.new("model/2", "1")
    @m2v2 = ModelWithKeyAndVersion.new("model/2", "2")
  end

  def test_fragment_cache_key
    assert_deprecated do
      assert_equal "views/what a key", @controller.fragment_cache_key("what a key")
      assert_equal "views/test.host/fragment_caching_test/some_action",
        @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
    end
  end

  def test_combined_fragment_cache_key
    assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key")
    assert_equal [ :views, "test.host/fragment_caching_test/some_action" ],
      @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
  end

  def test_read_fragment_with_caching_enabled
    @store.write("views/name", "value")
    assert_equal "value", @controller.read_fragment("name")
  end

  def test_read_fragment_with_caching_disabled
    @controller.perform_caching = false
    @store.write("views/name", "value")
    assert_nil @controller.read_fragment("name")
  end

  def test_read_fragment_with_versioned_model
    @controller.write_fragment([ "stuff", @m1v1 ], "hello")
    assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ])
    assert_nil @controller.read_fragment([ "stuff", @m1v2 ])
  end

  def test_fragment_exist_with_caching_enabled
    @store.write("views/name", "value")
    assert @controller.fragment_exist?("name")
    assert !@controller.fragment_exist?("other_name")
  end

  def test_fragment_exist_with_caching_disabled
    @controller.perform_caching = false
    @store.write("views/name", "value")
    assert !@controller.fragment_exist?("name")
    assert !@controller.fragment_exist?("other_name")
  end

  def test_write_fragment_with_caching_enabled
    assert_nil @store.read("views/name")
    assert_equal "value", @controller.write_fragment("name", "value")
    assert_equal "value", @store.read("views/name")
  end

  def test_write_fragment_with_caching_disabled
    assert_nil @store.read("views/name")
    @controller.perform_caching = false
    assert_equal "value", @controller.write_fragment("name", "value")
    assert_nil @store.read("views/name")
  end

  def test_expire_fragment_with_simple_key
    @store.write("views/name", "value")
    @controller.expire_fragment "name"
    assert_nil @store.read("views/name")
  end

  def test_expire_fragment_with_regexp
    @store.write("views/name", "value")
    @store.write("views/another_name", "another_value")
    @store.write("views/primalgrasp", "will not expire ;-)")

    @controller.expire_fragment(/name/)

    assert_nil @store.read("views/name")
    assert_nil @store.read("views/another_name")
    assert_equal "will not expire ;-)", @store.read("views/primalgrasp")
  end

  def test_fragment_for
    @store.write("views/expensive", "fragment content")
    fragment_computed = false

    view_context = @controller.view_context

    buffer = "generated till now -> ".html_safe
    buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }

    assert !fragment_computed
    assert_equal "generated till now -> fragment content", buffer
  end

  def test_html_safety
    assert_nil @store.read("views/name")
    content = "value".html_safe
    assert_equal content, @controller.write_fragment("name", content)

    cached = @store.read("views/name")
    assert_equal content, cached
    assert_equal String, cached.class

    html_safe = @controller.read_fragment("name")
    assert_equal content, html_safe
    assert html_safe.html_safe?
  end
end

class FunctionalCachingController < CachingController
  def fragment_cached
  end

  def html_fragment_cached_with_partial
    respond_to do |format|
      format.html
    end
  end

  def formatted_fragment_cached
    respond_to do |format|
      format.html
      format.xml
    end
  end

  def formatted_fragment_cached_with_variant
    request.variant = :phone if params[:v] == "phone"

    respond_to do |format|
      format.html.phone
      format.html
    end
  end

  def fragment_cached_without_digest
  end

  def fragment_cached_with_options
  end
end

class FunctionalFragmentCachingTest < ActionController::TestCase
  def setup
    super
    @store = ActiveSupport::Cache::MemoryStore.new
    @controller = FunctionalCachingController.new
    @controller.perform_caching = true
    @controller.cache_store = @store
    @controller.enable_fragment_cache_logging = true
  end

  def test_fragment_caching
    get :fragment_cached
    assert_response :success
    expected_body = <<-CACHED
Hello
This bit's fragment cached
Ciao
CACHED
    assert_equal expected_body, @response.body

    assert_equal "This bit's fragment cached",
      @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment")
  end

  def test_fragment_caching_in_partials
    get :html_fragment_cached_with_partial
    assert_response :success
    assert_match(/Old fragment caching in a partial/, @response.body)

    assert_match("Old fragment caching in a partial",
      @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial"))
  end

  def test_skipping_fragment_cache_digesting
    get :fragment_cached_without_digest, format: "html"
    assert_response :success
    expected_body = "<body>\n<p>ERB</p>\n</body>\n"

    assert_equal expected_body, @response.body
    assert_equal "<p>ERB</p>", @store.read("views/nodigest")
  end

  def test_fragment_caching_with_options
    time = Time.now
    get :fragment_cached_with_options
    assert_response :success
    expected_body = "<body>\n<p>ERB</p>\n</body>\n"

    assert_equal expected_body, @response.body
    Time.stub(:now, time + 11) do
      assert_nil @store.read("views/with_options")
    end
  end

  def test_render_inline_before_fragment_caching
    get :inline_fragment_cached
    assert_response :success
    assert_match(/Some inline content/, @response.body)
    assert_match(/Some cached content/, @response.body)
    assert_match("Some cached content",
      @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached"))
  end

  def test_fragment_cache_instrumentation
    payload = nil

    subscriber = proc do |*args|
      event = ActiveSupport::Notifications::Event.new(*args)
      payload = event.payload
    end

    ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do
      get :inline_fragment_cached
    end

    assert_equal "functional_caching", payload[:controller]
    assert_equal "inline_fragment_cached", payload[:action]
  end

  def test_html_formatted_fragment_caching
    get :formatted_fragment_cached, format: "html"
    assert_response :success
    expected_body = "<body>\n<p>ERB</p>\n</body>\n"

    assert_equal expected_body, @response.body

    assert_equal "<p>ERB</p>",
      @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
  end

  def test_xml_formatted_fragment_caching
    get :formatted_fragment_cached, format: "xml"
    assert_response :success
    expected_body = "<body>\n  <p>Builder</p>\n</body>\n"

    assert_equal expected_body, @response.body

    assert_equal "  <p>Builder</p>\n",
      @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
  end

  def test_fragment_caching_with_variant
    get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone }
    assert_response :success
    expected_body = "<body>\n<p>PHONE</p>\n</body>\n"

    assert_equal expected_body, @response.body

    assert_equal "<p>PHONE</p>",
      @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment")
  end

  private
    def template_digest(name)
      ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
    end
end

class CacheHelperOutputBufferTest < ActionController::TestCase
  class MockController
    def read_fragment(name, options)
      return false
    end

    def write_fragment(name, fragment, options)
      fragment
    end
  end

  def setup
    super
  end

  def test_output_buffer
    output_buffer = ActionView::OutputBuffer.new
    controller = MockController.new
    cache_helper = Class.new do
      def self.controller; end;
      def self.output_buffer; end;
      def self.output_buffer=; end;
    end
    cache_helper.extend(ActionView::Helpers::CacheHelper)

    cache_helper.stub :controller, controller do
      cache_helper.stub :output_buffer, output_buffer do
        assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
          assert_nothing_raised do
            cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
          end
        end
      end
    end
  end

  def test_safe_buffer
    output_buffer = ActiveSupport::SafeBuffer.new
    controller = MockController.new
    cache_helper = Class.new do
      def self.controller; end;
      def self.output_buffer; end;
      def self.output_buffer=; end;
    end
    cache_helper.extend(ActionView::Helpers::CacheHelper)

    cache_helper.stub :controller, controller do
      cache_helper.stub :output_buffer, output_buffer do
        assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
          assert_nothing_raised do
            cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
          end
        end
      end
    end
  end
end

class ViewCacheDependencyTest < ActionController::TestCase
  class NoDependenciesController < ActionController::Base
  end

  class HasDependenciesController < ActionController::Base
    view_cache_dependency { "trombone" }
    view_cache_dependency { "flute" }
  end

  def test_view_cache_dependencies_are_empty_by_default
    assert NoDependenciesController.new.view_cache_dependencies.empty?
  end

  def test_view_cache_dependencies_are_listed_in_declaration_order
    assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
  end
end

class CollectionCacheController < ActionController::Base
  attr_accessor :partial_rendered_times

  def index
    @customers = [Customer.new("david", params[:id] || 1)]
  end

  def index_ordered
    @customers = [Customer.new("david", 1), Customer.new("david", 2), Customer.new("david", 3)]
    render "index"
  end

  def index_explicit_render_in_controller
    @customers = [Customer.new("david", 1)]
    render partial: "customers/customer", collection: @customers, cached: true
  end

  def index_with_comment
    @customers = [Customer.new("david", 1)]
    render partial: "customers/commented_customer", collection: @customers, as: :customer, cached: true
  end

  def index_with_callable_cache_key
    @customers = [Customer.new("david", 1)]
    render partial: "customers/customer", collection: @customers, cached: -> customer { "cached_david" }
  end
end

class CollectionCacheTest < ActionController::TestCase
  def setup
    super
    @controller = CollectionCacheController.new
    @controller.perform_caching = true
    @controller.partial_rendered_times = 0
    @controller.cache_store = ActiveSupport::Cache::MemoryStore.new
    ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
  end

  def test_collection_fetches_cached_views
    get :index
    assert_equal 1, @controller.partial_rendered_times
    assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1")

    get :index
    assert_equal 1, @controller.partial_rendered_times
  end

  def test_preserves_order_when_reading_from_cache_plus_rendering
    get :index, params: { id: 2 }
    assert_equal 1, @controller.partial_rendered_times
    assert_select ":root", "david, 2"

    get :index_ordered
    assert_equal 3, @controller.partial_rendered_times
    assert_select ":root", "david, 1\n  david, 2\n  david, 3"
  end

  def test_explicit_render_call_with_options
    get :index_explicit_render_in_controller

    assert_select ":root", "david, 1"
  end

  def test_caching_works_with_beginning_comment
    get :index_with_comment
    assert_equal 1, @controller.partial_rendered_times

    get :index_with_comment
    assert_equal 1, @controller.partial_rendered_times
  end

  def test_caching_with_callable_cache_key
    get :index_with_callable_cache_key
    assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david")
  end
end

class FragmentCacheKeyTestController < CachingController
  attr_accessor :account_id

  fragment_cache_key "v1"
  fragment_cache_key { account_id }
end

class FragmentCacheKeyTest < ActionController::TestCase
  def setup
    super
    @store = ActiveSupport::Cache::MemoryStore.new
    @controller = FragmentCacheKeyTestController.new
    @controller.perform_caching = true
    @controller.cache_store = @store
  end

  def test_combined_fragment_cache_key
    @controller.account_id = "123"
    assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key")

    @controller.account_id = nil
    assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
  end

  def test_combined_fragment_cache_key_with_envs
    ENV["RAILS_APP_VERSION"] = "55"
    assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")

    ENV["RAILS_CACHE_ID"] = "66"
    assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
  ensure
    ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil
  end
end