Merge branch 'TUR-61-fix-final-match-save' into 'master'

TUR-61 Fix final match completion after group stage

Closes TUR-61

See merge request turniere/turniere-backend!35
This commit is contained in:
Daniel Schädler 2026-04-12 16:28:29 +00:00
commit 84cdb230fb
6 changed files with 34 additions and 34 deletions

View File

@ -70,7 +70,9 @@ class PlayoffStageService
# @return [Array] the objects that changed and need to be saved # @return [Array] the objects that changed and need to be saved
def populate_match_below(current_match) def populate_match_below(current_match)
current_stage = current_match.stage 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 does not exist (there are no matches after the finale)
return [] if next_stage.nil? return [] if next_stage.nil?

View File

@ -7,7 +7,6 @@ require 'time'
module TurniereE2E module TurniereE2E
class ScenarioRunner class ScenarioRunner
DEFAULT_PASSWORD = 'password123'.freeze DEFAULT_PASSWORD = 'password123'.freeze
FINAL_FINISH_ERROR = 'Moving Team one stage down failed'
attr_reader :client 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 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) final_match = final_match_of!(tournament)
finish_attempt = play_match!( finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match))
final_match,
winner_name: preferred_playoff_winner_name(final_match),
expected_finish_status: 422,
expected_finish_error: FINAL_FINISH_ERROR
)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:final_finish_attempted] = { checkpoints[:final_finish_attempted] = {
finish_attempt: finish_attempt, 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 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) final_match = final_match_of!(tournament)
finish_attempt = play_match!( finish_attempt = play_match!(final_match, winner_name: preferred_playoff_winner_name(final_match))
final_match,
winner_name: preferred_playoff_winner_name(final_match),
expected_finish_status: 422,
expected_finish_error: FINAL_FINISH_ERROR
)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:final_finish_attempted] = { checkpoints[:final_finish_attempted] = {
finish_attempt: finish_attempt, finish_attempt: finish_attempt,

View File

@ -195,7 +195,9 @@ RSpec.describe MatchesController, type: :controller do
context 'PopulateMatchBelowAndSave fails' do context 'PopulateMatchBelowAndSave fails' do
before 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) .and_return(context)
end end
@ -204,7 +206,7 @@ RSpec.describe MatchesController, type: :controller do
it 'returns unprocessable entity' do it 'returns unprocessable entity' do
# mock match.winner to be the first team of the match to skip winner check here # 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) put :update, params: { id: @running_playoff_match.to_param }.merge(finished)
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)

View File

@ -21,7 +21,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
) )
end 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 result = runner.run_group_stage_main_usecase
expect(result.dig(:checkpoints, :created, :playoff_teams_amount)).to eq(8) 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) expect(playoff_entry_stage[:match_states].count).to eq(4)
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
expect(final_attempt[:status]).to eq(422) expect(final_attempt[:status]).to eq(200)
expect(final_attempt.dig(:body, :error)).to eq('Moving Team one stage down failed') expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries)
.find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished'])
end 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 result = runner.run_group_stage_intermediate_round
created = result.fetch(:checkpoints).fetch(:created) 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) expect(playoff_entry_stage[:match_states].count('not_started')).to eq(1)
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
expect(final_attempt[:status]).to eq(422) expect(final_attempt[:status]).to eq(200)
expect(final_attempt.dig(:body, :error)).to eq('Moving Team one stage down failed') expect(result.dig(:checkpoints, :final_finish_attempted, :tournament, :stage_summaries)
.find { |stage| stage[:level] == 0 }[:match_states]).to eq(['finished'])
end end
{ {

View File

@ -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. 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` ## 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`: Observed and covered by `spec/e2e/http/tournament_lifecycle_spec.rb` and `script/e2e_scenarios.rb`:

View File

@ -212,5 +212,22 @@ RSpec.describe PlayoffStageService do
expect(@test.state).to eq('not_started') expect(@test.state).to eq('not_started')
end end
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
end end