diff --git a/app/views/schools/_wrestler_row_cells.html.erb b/app/views/schools/_wrestler_row_cells.html.erb new file mode 100644 index 0000000..30d13c0 --- /dev/null +++ b/app/views/schools/_wrestler_row_cells.html.erb @@ -0,0 +1,13 @@ +<% if local_assigns[:school_permission_key].present? %> + <% wrestler_path_with_key = wrestler_path(wrestler) %> + <% wrestler_path_with_key += "?school_permission_key=#{school_permission_key}" %> +
<%= match.bout_number %> <%= match.bracket_score_string %>
<%= @winner_place %> Place Winner
<%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>
<%= @winner_place %> Place Winner
<%= match.bout_number %> <%= match.bracket_score_string %>
<%= @winner_place %> Place Winner
<%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>
<%= @winner_place %> Place Winner
| <%= @schools.index(school) + 1 %>. <%= school.name %> (<%= school.abbreviation %>) | -<%= school.page_score_string %> | -
*All wrestlers without a seed (determined by tournament director) will be assigned a random bracket line.
diff --git a/test/controllers/school_show_cache_test.rb b/test/controllers/school_show_cache_test.rb new file mode 100644 index 0000000..e5ed7b0 --- /dev/null +++ b/test/controllers/school_show_cache_test.rb @@ -0,0 +1,143 @@ +require "test_helper" + +class SchoolShowCacheTest < ActionController::TestCase + tests SchoolsController + + setup do + create_double_elim_tournament_single_weight_1_6(8) + @tournament.update!(user_id: users(:one).id) + @school = @tournament.schools.first + + sign_in users(:one) + + @original_perform_caching = ActionController::Base.perform_caching + ActionController::Base.perform_caching = true + Rails.cache.clear + end + + teardown do + Rails.cache.clear + ActionController::Base.perform_caching = @original_perform_caching + end + + test "school show wrestler cell fragments hit cache and invalidate after wrestler update" do + first_events = cache_events_for_school_show do + get :show, params: { id: @school.id } + assert_response :success + end + assert_operator cache_writes(first_events), :>, 0, "Expected initial school show render to write wrestler cell fragments" + + second_events = cache_events_for_school_show do + get :show, params: { id: @school.id } + assert_response :success + end + assert_equal 0, cache_writes(second_events), "Expected repeat school show render to reuse wrestler cell fragments" + assert_operator cache_hits(second_events), :>, 0, "Expected repeat school show render to hit wrestler cell cache" + + wrestler = @school.wrestlers.first + third_events = cache_events_for_school_show do + wrestler.touch + get :show, params: { id: @school.id } + assert_response :success + end + assert_operator cache_writes(third_events), :>, 0, "Expected wrestler update to invalidate school show wrestler cell cache" + end + + test "school show does not leak manage-only controls from cache across users" do + get :show, params: { id: @school.id } + assert_response :success + assert_includes response.body, "New Wrestler" + assert_match(/fa-trash-alt/, response.body) + assert_match(/fa-edit/, response.body) + + sign_out + + spectator_events = cache_events_for_school_show do + get :show, params: { id: @school.id } + assert_response :success + end + assert_operator cache_hits(spectator_events), :>, 0, "Expected spectator request to hit wrestler cell cache warmed by owner" + assert_not_includes response.body, "New Wrestler" + assert_no_match(/fa-trash-alt/, response.body) + assert_no_match(/fa-edit/, response.body) + end + + test "school show with school_permission_key bypasses cached wrestler cell fragments" do + @school.update!(permission_key: SecureRandom.uuid) + sign_out + + key_request_events = cache_events_for_school_show do + get :show, params: { id: @school.id, school_permission_key: @school.permission_key } + assert_response :success + end + + assert_equal 0, cache_writes(key_request_events), "Expected school_permission_key request to bypass cached wrestler cells" + assert_equal 0, cache_hits(key_request_events), "Expected school_permission_key request to avoid reading cached wrestler cells" + end + + test "completing a match expires school show wrestler cell caches" do + warm_events = cache_events_for_school_show do + get :show, params: { id: @school.id } + assert_response :success + end + assert_operator cache_writes(warm_events), :>, 0, "Expected initial school show render to warm wrestler cell cache" + + wrestler = @school.wrestlers.first + assert wrestler, "Expected a wrestler for match-completion cache test" + match = wrestler.unfinished_matches.first || wrestler.all_matches.first + assert match, "Expected a match involving school wrestler" + + winner_id = match.w1 || match.w2 + assert winner_id, "Expected match to have at least one wrestler slot" + match.update!( + finished: 1, + winner_id: winner_id, + win_type: "Decision", + score: "1-0" + ) + + post_action_events = cache_events_for_school_show do + get :show, params: { id: @school.id } + assert_response :success + end + assert_operator cache_writes(post_action_events), :>, 0, "Expected completed match to expire school show wrestler cell cache" + end + + private + + def sign_out + @request.session[:user_id] = nil + @controller.instance_variable_set(:@current_user, nil) + @controller.instance_variable_set(:@current_ability, nil) + end + + def cache_events_for_school_show + events = [] + subscriber = lambda do |name, _start, _finish, _id, payload| + key = payload[:key].to_s + next unless key.include?("school_show_wrestler_cells") + + events << { name: name, hit: payload[:hit] } + end + + ActiveSupport::Notifications.subscribed( + subscriber, + /cache_(read|write|fetch_hit|generate)\.active_support/ + ) do + yield + end + + events + end + + def cache_writes(events) + events.count { |event| event[:name] == "cache_write.active_support" } + end + + def cache_hits(events) + events.count do |event| + event[:name] == "cache_fetch_hit.active_support" || + (event[:name] == "cache_read.active_support" && event[:hit]) + end + end +end diff --git a/test/controllers/tournament_pages_cache_test.rb b/test/controllers/tournament_pages_cache_test.rb new file mode 100644 index 0000000..365f25f --- /dev/null +++ b/test/controllers/tournament_pages_cache_test.rb @@ -0,0 +1,164 @@ +require "test_helper" + +class TournamentPagesCacheTest < ActionController::TestCase + tests TournamentsController + + setup do + create_double_elim_tournament_single_weight_1_6(8) + @tournament.update!(user_id: users(:one).id) + @weight = @tournament.weights.first + + sign_in users(:one) + + @original_perform_caching = ActionController::Base.perform_caching + ActionController::Base.perform_caching = true + Rails.cache.clear + end + + teardown do + Rails.cache.clear + ActionController::Base.perform_caching = @original_perform_caching + end + + test "team_scores cache hits on repeat render and rewrites after school update" do + first_events = cache_events_for(%w[team_scores team_score_row]) do + get :team_scores, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(first_events), :>, 0, "Expected initial team_scores render to write fragments" + + second_events = cache_events_for(%w[team_scores team_score_row]) do + get :team_scores, params: { id: @tournament.id } + assert_response :success + end + assert_equal 0, cache_writes(second_events), "Expected repeat team_scores render to reuse fragments" + assert_operator cache_hits(second_events), :>, 0, "Expected repeat team_scores render to hit cache" + + school = @tournament.schools.first + third_events = cache_events_for(%w[team_scores team_score_row]) do + school.update!(score: (school.score || 0) + 1) + get :team_scores, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(third_events), :>, 0, "Expected school score update to invalidate team_scores cache" + end + + test "bracket cache hits on repeat render and rewrites after match update" do + key_markers = [@weight.id.to_s + "_bracket", "bracket_round_match", "bracket_final_match"] + + first_events = cache_events_for(key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_operator cache_writes(first_events), :>, 0, "Expected initial bracket render to write fragments" + + second_events = cache_events_for(key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_equal 0, cache_writes(second_events), "Expected repeat bracket render to reuse fragments" + assert_operator cache_hits(second_events), :>, 0, "Expected repeat bracket render to hit cache" + + match = @weight.matches.first + third_events = cache_events_for(key_markers) do + match.touch + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_operator cache_writes(third_events), :>, 0, "Expected match update to invalidate bracket cache" + end + + test "bracket cache separates print and non-print variants" do + key_markers = [@weight.id.to_s + "_bracket"] + + non_print_events = cache_events_for(key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_operator cache_writes(non_print_events), :>, 0, "Expected non-print bracket render to write a page fragment" + assert_match(%r{\/matches\/\d+\/spectate}, response.body, "Expected non-print bracket view to include spectate links") + + first_print_events = cache_events_for(key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id, print: true } + assert_response :success + end + assert_operator cache_writes(first_print_events), :>, 0, "Expected first print bracket render to write a separate page fragment" + assert_no_match(%r{\/matches\/\d+\/spectate}, response.body, "Expected print bracket view to omit spectate links") + + second_print_events = cache_events_for(key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id, print: true } + assert_response :success + end + assert_equal 0, cache_writes(second_print_events), "Expected repeat print bracket render to reuse print cache fragment" + assert_operator cache_hits(second_print_events), :>, 0, "Expected repeat print bracket render to hit cache" + end + + test "completing a match expires team_scores and bracket caches" do + team_warm_events = cache_events_for(%w[team_scores team_score_row]) do + get :team_scores, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(team_warm_events), :>, 0, "Expected initial team_scores render to warm cache" + + bracket_key_markers = [@weight.id.to_s + "_bracket", "bracket_round_match", "bracket_final_match"] + bracket_warm_events = cache_events_for(bracket_key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_operator cache_writes(bracket_warm_events), :>, 0, "Expected initial bracket render to warm cache" + + match = @weight.matches.where(finished: [nil, 0]).first || @weight.matches.first + assert match, "Expected a match to complete for expiration test" + + match.update!( + finished: 1, + winner_id: match.w1 || match.w2, + win_type: "Decision", + score: "1-0" + ) + + team_post_events = cache_events_for(%w[team_scores team_score_row]) do + get :team_scores, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(team_post_events), :>, 0, "Expected completed match to expire team_scores cache" + + bracket_post_events = cache_events_for(bracket_key_markers) do + get :bracket, params: { id: @tournament.id, weight: @weight.id } + assert_response :success + end + assert_operator cache_writes(bracket_post_events), :>, 0, "Expected completed match to expire bracket cache" + end + + private + + def cache_events_for(key_markers) + events = [] + subscriber = lambda do |name, _start, _finish, _id, payload| + key = payload[:key].to_s + next unless key_markers.any? { |marker| key.include?(marker) } + + events << { name: name, hit: payload[:hit] } + end + + ActiveSupport::Notifications.subscribed( + subscriber, + /cache_(read|write|fetch_hit|generate)\.active_support/ + ) do + yield + end + + events + end + + def cache_writes(events) + events.count { |event| event[:name] == "cache_write.active_support" } + end + + def cache_hits(events) + events.count do |event| + event[:name] == "cache_fetch_hit.active_support" || + (event[:name] == "cache_read.active_support" && event[:hit]) + end + end +end diff --git a/test/controllers/up_matches_cache_test.rb b/test/controllers/up_matches_cache_test.rb index f48d341..27d45d1 100644 --- a/test/controllers/up_matches_cache_test.rb +++ b/test/controllers/up_matches_cache_test.rb @@ -73,13 +73,86 @@ class UpMatchesCacheTest < ActionController::TestCase assert_operator cache_hits(repeat_events), :>, 0, "Expected cache hits after queue clear rewrite" end + test "up_matches unassigned row fragments hit cache and invalidate after unassigned match update" do + key_markers = %w[up_matches_unassigned_row] + + first_events = cache_events_for_up_matches(key_markers) do + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(first_events), :>, 0, "Expected initial unassigned row render to write fragments" + + second_events = cache_events_for_up_matches(key_markers) do + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + assert_equal 0, cache_writes(second_events), "Expected repeat unassigned row render to reuse cached fragments" + assert_operator cache_hits(second_events), :>, 0, "Expected repeat unassigned row render to hit cache" + + unassigned_match = @tournament.up_matches_unassigned_matches.first + assert unassigned_match, "Expected at least one unassigned match for cache invalidation test" + + third_events = cache_events_for_up_matches(key_markers) do + unassigned_match.touch + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(third_events), :>, 0, "Expected unassigned match update to invalidate unassigned row fragment" + end + + test "completing an on-mat match expires up_matches cached fragments" do + warm_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(warm_events), :>, 0, "Expected initial up_matches render to warm caches" + + mat = @tournament.mats.detect { |m| m.queue1_match.present? } + assert mat, "Expected a mat with a queued match" + match = mat.queue1_match + assert match, "Expected queue1 match to complete" + + post_action_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do + match.update!( + finished: 1, + winner_id: match.w1 || match.w2, + win_type: "Decision", + score: "1-0" + ) + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + + assert_operator cache_writes(post_action_events), :>, 0, "Expected completed match to expire and rewrite up_matches caches" + end + + test "manually assigning an unassigned match to a mat queue expires up_matches caches" do + warm_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + assert_operator cache_writes(warm_events), :>, 0, "Expected initial up_matches render to warm caches" + + unassigned_match = @tournament.up_matches_unassigned_matches.first + assert unassigned_match, "Expected at least one unassigned match to manually place on a mat" + target_mat = @tournament.mats.first + + post_action_events = cache_events_for_up_matches(%w[up_matches_mat_row up_matches_unassigned_row]) do + target_mat.assign_match_to_queue!(unassigned_match, 4) + get :up_matches, params: { id: @tournament.id } + assert_response :success + end + + assert_operator cache_writes(post_action_events), :>, 0, "Expected manual mat assignment to expire and rewrite up_matches caches" + end + private - def cache_events_for_up_matches + def cache_events_for_up_matches(key_markers = %w[up_matches_mat_row up_matches_unassigned_row]) events = [] subscriber = lambda do |name, _start, _finish, _id, payload| key = payload[:key].to_s - next unless key.include?("up_matches_mat_row") + next unless key_markers.any? { |marker| key.include?(marker) } events << { name: name, hit: payload[:hit] } end diff --git a/test/controllers/weight_show_cache_test.rb b/test/controllers/weight_show_cache_test.rb new file mode 100644 index 0000000..0a2640a --- /dev/null +++ b/test/controllers/weight_show_cache_test.rb @@ -0,0 +1,107 @@ +require "test_helper" + +class WeightShowCacheTest < ActionController::TestCase + tests WeightsController + + setup do + create_a_tournament_with_single_weight("Regular Double Elimination 1-6", 8) + @tournament.update!(user_id: users(:one).id) + @weight = @tournament.weights.first + + @original_perform_caching = ActionController::Base.perform_caching + ActionController::Base.perform_caching = true + Rails.cache.clear + end + + teardown do + Rails.cache.clear + ActionController::Base.perform_caching = @original_perform_caching + end + + test "weight show readonly row fragments hit cache and invalidate after wrestler update" do + first_events = cache_events_for_weight_show do + get :show, params: { id: @weight.id } + assert_response :success + end + assert_operator cache_writes(first_events), :>, 0, "Expected initial weight show render to write readonly row fragments" + + second_events = cache_events_for_weight_show do + get :show, params: { id: @weight.id } + assert_response :success + end + assert_equal 0, cache_writes(second_events), "Expected repeat weight show render to reuse readonly row fragments" + assert_operator cache_hits(second_events), :>, 0, "Expected repeat weight show render to hit readonly row cache" + + wrestler = @weight.wrestlers.first + third_events = cache_events_for_weight_show do + wrestler.touch + get :show, params: { id: @weight.id } + assert_response :success + end + assert_operator cache_writes(third_events), :>, 0, "Expected wrestler update to invalidate weight show readonly row cache" + end + + test "weight show does not leak manage-only controls from cache across users" do + sign_in users(:one) + get :show, params: { id: @weight.id } + assert_response :success + assert_includes response.body, "Save Seeds" + assert_match(/fa-trash-alt/, response.body) + assert_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body) + + sign_out + + get :show, params: { id: @weight.id } + assert_response :success + assert_not_includes response.body, "Save Seeds" + assert_no_match(/fa-trash-alt/, response.body) + assert_no_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body) + + spectator_cache_events = cache_events_for_weight_show do + get :show, params: { id: @weight.id } + assert_response :success + end + assert_operator cache_hits(spectator_cache_events), :>, 0, "Expected repeat spectator request to hit readonly wrestler row cache" + assert_not_includes response.body, "Save Seeds" + assert_no_match(/fa-trash-alt/, response.body) + assert_no_match(/name="wrestler\[\d+\]\[original_seed\]"/, response.body) + end + + private + + def sign_out + @request.session[:user_id] = nil + @controller.instance_variable_set(:@current_user, nil) + @controller.instance_variable_set(:@current_ability, nil) + end + + def cache_events_for_weight_show + events = [] + subscriber = lambda do |name, _start, _finish, _id, payload| + key = payload[:key].to_s + next unless key.include?("weight_show_wrestler_row") + + events << { name: name, hit: payload[:hit] } + end + + ActiveSupport::Notifications.subscribed( + subscriber, + /cache_(read|write|fetch_hit|generate)\.active_support/ + ) do + yield + end + + events + end + + def cache_writes(events) + events.count { |event| event[:name] == "cache_write.active_support" } + end + + def cache_hits(events) + events.count do |event| + event[:name] == "cache_fetch_hit.active_support" || + (event[:name] == "cache_read.active_support" && event[:hit]) + end + end +end