diff --git a/test/test_conditional_get_user_scoped_etag.rb b/test/test_conditional_get_user_scoped_etag.rb new file mode 100644 index 0000000..48d7066 --- /dev/null +++ b/test/test_conditional_get_user_scoped_etag.rb @@ -0,0 +1,93 @@ +require 'minitest/autorun' +require 'prism' +require 'support' + +# Session 42's documented "etag/current_user audit" debt, closed here after +# being deferred across sessions 42-61. The audit itself: grep every +# fresh_when call in examples/blog/app/controllers and check whether the +# view it guards renders anything current_user-dependent. Two of the three +# call sites already included current_user&.id in their etag components +# (PostsController#show, CommentsController#index) - PostsController#index +# was the one real gap, found and fixed directly in +# examples/blog/app/controllers/posts_controller.rb: its view gates the +# "new post" form and the layout's signed-in-as banner on +# user_signed_in?/current_user, but the etag never accounted for viewer +# identity, so a shared cache keying on URL+ETag alone (no per-user Vary) +# could serve one visitor's signed-in body back as a 304 to a different, or +# signed-out, visitor whose If-None-Match happened to match. +# +# This file does not require examples/blog/app.rb directly - see +# test_integration_preview.rb's fixture comment: that file calls +# Prism::Orm.connect as a process-wide global that would leak into every +# other test in this suite. Instead it reproduces the exact shape of the +# bug (buggy_index) and the fix (fixed_index) against an isolated app, the +# same convention test_cors_conditional_get.rb already established for +# ConditionalGet. +class ScopedIndexController < Prism::Controller + def buggy_index + viewer = request.env['HTTP_X_TEST_USER'] + fresh_when(etag: ['same-page-content']) # the bug: omits viewer identity + html(viewer ? "signed in as #{viewer}" : 'anonymous visitor') + end + + def fixed_index + viewer = request.env['HTTP_X_TEST_USER'] + fresh_when(etag: ['same-page-content', viewer]) # the fix + html(viewer ? "signed in as #{viewer}" : 'anonymous visitor') + end +end + +class ScopedIndexApp < Prism::Application + get '/buggy', to: 'scoped_index#buggy_index' + get '/fixed', to: 'scoped_index#fixed_index' +end + +class ConditionalGetUserScopedEtagTest < Minitest::Test + include TestSupport + + def call(path, headers = {}) + ScopedIndexApp.call(env_for('GET', path, headers: headers)) + end + + def test_buggy_etag_is_identical_across_different_viewers + _, anon_headers, = call('/buggy') + _, alice_headers, = call('/buggy', 'X-Test-User' => 'alice') + + assert_equal anon_headers['ETag'], alice_headers['ETag'], + 'demonstrates the bug: rendered content differs by viewer but the etag does not' + end + + def test_buggy_etag_lets_a_cached_signed_in_body_leak_back_to_an_anonymous_request + _, alice_headers, alice_body = call('/buggy', 'X-Test-User' => 'alice') + assert_equal 'signed in as alice', alice_body.join + + status, _, anon_body = call('/buggy', 'If-None-Match' => alice_headers['ETag']) + assert_equal 304, status, + "the bug: an anonymous visitor whose If-None-Match happens to equal alice's is told nothing changed" + assert_equal '', anon_body.join + end + + def test_fixed_etag_differs_by_viewer + _, anon_headers, = call('/fixed') + _, alice_headers, = call('/fixed', 'X-Test-User' => 'alice') + + refute_equal anon_headers['ETag'], alice_headers['ETag'] + end + + def test_fixed_etag_never_returns_a_304_across_different_viewers + _, alice_headers, = call('/fixed', 'X-Test-User' => 'alice') + + status, _, anon_body = call('/fixed', 'If-None-Match' => alice_headers['ETag']) + assert_equal 200, status, "an anonymous visitor must get a fresh render, never alice's cached 304" + assert_equal 'anonymous visitor', anon_body.join + end + + def test_fixed_etag_still_returns_304_for_the_same_viewer + _, alice_headers, = call('/fixed', 'X-Test-User' => 'alice') + + status, headers, body = call('/fixed', 'X-Test-User' => 'alice', 'If-None-Match' => alice_headers['ETag']) + assert_equal 304, status + assert_equal '', body.join + assert_equal alice_headers['ETag'], headers['ETag'] + end +end diff --git a/test/test_content_for.rb b/test/test_content_for.rb new file mode 100644 index 0000000..fcd78f4 --- /dev/null +++ b/test/test_content_for.rb @@ -0,0 +1,71 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' + +class ContentForTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-content-for') + FileUtils.mkdir_p(File.join(@dir, 'pages')) + FileUtils.mkdir_p(File.join(@dir, 'layouts')) + @view = Prism::View.new(@dir) + end + + def teardown + FileUtils.remove_entry(@dir) + end + + def write(relative, source) + File.write(File.join(@dir, relative), source) + end + + def test_content_for_slot_is_readable_in_the_layout_rendered_after_it + write('pages/show.erb', '<% content_for(:title) do %>Custom Title<% end %>Body text') + write('layouts/application.erb', '[<%= content_for(:title) %>] <%== content %>') + + result = @view.render('pages/show', layout: :application) + assert_equal '[Custom Title] Body text', result + end + + def test_content_for_without_a_call_reads_back_empty_string + write('pages/show.erb', 'Body only') + write('layouts/application.erb', 'title=[<%= content_for(:title) %>]<%== content %>') + + result = @view.render('pages/show', layout: :application) + assert_equal 'title=[]Body only', result + end + + def test_content_for_question_mark_reports_presence + write('pages/show.erb', '<% content_for(:sidebar) do %>widgets<% end %>') + write('layouts/application.erb', '<% if content_for?(:sidebar) %>has-sidebar<% else %>no-sidebar<% end %>') + + assert_equal 'has-sidebar', @view.render('pages/show', layout: :application) + end + + def test_content_for_question_mark_is_false_when_never_set + write('pages/show.erb', 'nothing here') + write('layouts/application.erb', '<% if content_for?(:sidebar) %>has-sidebar<% else %>no-sidebar<% end %>') + + assert_equal 'no-sidebar', @view.render('pages/show', layout: :application) + end + + def test_multiple_content_for_calls_for_the_same_slot_accumulate + write('pages/show.erb', <<~ERB) + <% content_for(:scripts) do %>a.js<% end %> + <% content_for(:scripts) do %>b.js<% end %> + ERB + write('layouts/application.erb', '<%= content_for(:scripts) %>') + + assert_equal 'a.jsb.js', @view.render('pages/show', layout: :application) + end + + def test_content_for_state_does_not_leak_between_separate_render_calls + write('pages/one.erb', '<% content_for(:title) do %>One<% end %>') + write('pages/two.erb', 'no title here') + write('layouts/application.erb', '[<%= content_for(:title) %>]<%== content %>') + + @view.render('pages/one', layout: :application) + result = @view.render('pages/two', layout: :application) + assert_equal '[]no title here', result + end +end