From 6d21dad5d7abaa8dcc31ca2bb029dd11f99eadf5 Mon Sep 17 00:00:00 2001 From: Malaber Date: Sun, 12 Apr 2026 18:07:08 +0200 Subject: [PATCH 1/2] Fix final match completion after group stage --- app/services/playoff_stage_service.rb | 4 +++- e2e/lib/scenario_runner.rb | 15 ++------------- spec/e2e/http/tournament_lifecycle_spec.rb | 14 ++++++++------ spec/e2e_known_behaviors.md | 12 ------------ spec/services/playoff_stage_service_spec.rb | 17 +++++++++++++++++ 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index e12ae2c..6cef460 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -70,7 +70,9 @@ class PlayoffStageService # @return [Array] the objects that changed and need to be saved def populate_match_below(current_match) current_stage = current_match.stage - next_stage = current_stage.tournament.stages.find { |s| s.level == current_stage.level - 1 } + next_stage = current_stage.tournament.stages + .select { |stage| stage.matches.any? && stage.level < current_stage.level } + .max_by(&:level) # return if next stage does not exist (there are no matches after the finale) return [] if next_stage.nil? diff --git a/e2e/lib/scenario_runner.rb b/e2e/lib/scenario_runner.rb index f310070..d11a3d8 100644 --- a/e2e/lib/scenario_runner.rb +++ b/e2e/lib/scenario_runner.rb @@ -7,7 +7,6 @@ require 'time' module TurniereE2E class ScenarioRunner DEFAULT_PASSWORD = 'password123'.freeze - FINAL_FINISH_ERROR = 'Moving Team one stage down failed' attr_reader :client @@ -65,12 +64,7 @@ module TurniereE2E return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt final_match = final_match_of!(tournament) - finish_attempt = play_match!( - final_match, - winner_name: preferred_playoff_winner_name(final_match), - expected_finish_status: 422, - expected_finish_error: FINAL_FINISH_ERROR - ) + finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { finish_attempt: finish_attempt, @@ -103,12 +97,7 @@ module TurniereE2E return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt final_match = final_match_of!(tournament) - finish_attempt = play_match!( - final_match, - winner_name: preferred_playoff_winner_name(final_match), - expected_finish_status: 422, - expected_finish_error: FINAL_FINISH_ERROR - ) + finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { finish_attempt: finish_attempt, diff --git a/spec/e2e/http/tournament_lifecycle_spec.rb b/spec/e2e/http/tournament_lifecycle_spec.rb index ac8046c..04edcb1 100644 --- a/spec/e2e/http/tournament_lifecycle_spec.rb +++ b/spec/e2e/http/tournament_lifecycle_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do ) end - it 'drives the main 4x4 group-stage use case over HTTP and preserves the current final-finish behavior' do + it 'drives the main 4x4 group-stage use case over HTTP through a finished final' do result = runner.run_group_stage_main_usecase expect(result.dig(:checkpoints, :created, :playoff_teams_amount)).to eq(8) @@ -35,11 +35,12 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do expect(playoff_entry_stage[:match_states].count).to eq(4) final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) - expect(final_attempt[:status]).to eq(422) - expect(final_attempt.dig(:body, :error)).to eq('Moving Team one stage down failed') + expect(final_attempt[:status]).to eq(200) + expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries) + .find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished']) end - it 'drives the 3-group intermediate-round path over HTTP and preserves the current advancing behavior' do + it 'drives the 3-group intermediate-round path over HTTP through a finished final' do result = runner.run_group_stage_intermediate_round created = result.fetch(:checkpoints).fetch(:created) @@ -55,8 +56,9 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do expect(playoff_entry_stage[:match_states].count('not_started')).to eq(1) final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) - expect(final_attempt[:status]).to eq(422) - expect(final_attempt.dig(:body, :error)).to eq('Moving Team one stage down failed') + expect(final_attempt[:status]).to eq(200) + expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries) + .find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished']) end { diff --git a/spec/e2e_known_behaviors.md b/spec/e2e_known_behaviors.md index fccc3d4..d6154ca 100644 --- a/spec/e2e_known_behaviors.md +++ b/spec/e2e_known_behaviors.md @@ -20,18 +20,6 @@ The reusable scenario script supports this through: Frontend tests can use the same script against a live backend as long as those credentials point to a confirmed account on that backend. -## Group-stage tournaments cannot finish the playoff final through the normal match endpoint - -Observed and covered by `spec/e2e/http/tournament_lifecycle_spec.rb` and `script/e2e_scenarios.rb`: - -- in tournaments that started with a group stage, the playoff final can be started and scored -- but `PATCH /matches/:id` with `state=finished` on that final returns: - - status `422` - - error `"Moving Team one stage down failed"` -- the final remains `in_progress` - -This appears to happen because playoff stages count down to level `0`, while the old group stage still exists at level `-1`. The final-finish flow looks for a "stage below" using `current_stage.level - 1`, which resolves to the group stage for finals in group-stage tournaments. The E2E suite preserves this current behavior instead of changing backend code. - ## Group stage to playoff conversion with 3 groups and `playoff_teams_amount = 4` Observed and covered by `spec/e2e/http/tournament_lifecycle_spec.rb` and `script/e2e_scenarios.rb`: diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 2e6e9c3..721b7d1 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -212,5 +212,22 @@ RSpec.describe PlayoffStageService do expect(@test.state).to eq('not_started') end end + + context 'when the current match is the final of a tournament with a group stage' do + before do + @tournament = create(:group_stage_tournament, stage_count: 1) + @final_match = @tournament.stages.reject { |stage| stage.level == -1 }.max_by(&:level).matches.first + @final_match.state = :finished + @final_match.match_scores.each_with_index do |match_score, index| + match_score.points = index + match_score.save! + end + @final_match.save! + end + + it 'does not treat the group stage as a lower playoff stage' do + expect(PlayoffStageService.populate_match_below(@final_match)).to eq([]) + end + end end end From 11b1004053c090d3871b0551e50e13c2a40e50df Mon Sep 17 00:00:00 2001 From: Malaber Date: Sun, 12 Apr 2026 18:19:28 +0200 Subject: [PATCH 2/2] Stabilize match controller failure spec --- spec/controllers/matches_controller_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/controllers/matches_controller_spec.rb b/spec/controllers/matches_controller_spec.rb index 2d9482b..a283b63 100644 --- a/spec/controllers/matches_controller_spec.rb +++ b/spec/controllers/matches_controller_spec.rb @@ -195,7 +195,9 @@ RSpec.describe MatchesController, type: :controller do context 'PopulateMatchBelowAndSave fails' do before do - expect(PopulateMatchBelowAndSave).to receive(:call).once.with(match: @running_playoff_match) + expect(PopulateMatchBelowAndSave).to receive(:call).once.with( + match: have_attributes(id: @running_playoff_match.id) + ) .and_return(context) end @@ -204,7 +206,7 @@ RSpec.describe MatchesController, type: :controller do it 'returns unprocessable entity' do # mock match.winner to be the first team of the match to skip winner check here - allow(@running_playoff_match).to receive(:winner).and_return(@running_playoff_match.match_scores.first.team) + allow_any_instance_of(Match).to receive(:winner).and_return(@running_playoff_match.match_scores.first.team) put :update, params: { id: @running_playoff_match.to_param }.merge(finished) expect(response).to have_http_status(:unprocessable_entity)