turniere-backend/spec/requests/e2e/tournament_lifecycle_spec.rb

299 lines
10 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Tournament lifecycle E2E', type: :request do
def parsed_response
JSON.parse(response.body, symbolize_names: true)
end
def auth_headers_for(user)
user.create_new_auth_token.merge(
'ACCEPT' => 'application/json'
)
end
def create_tournament!(user:, params:)
post '/tournaments', params:, headers: auth_headers_for(user), as: :json
expect(response).to have_http_status(:created)
Tournament.find(parsed_response[:id])
end
def update_timer_by_seconds!(tournament, user, seconds)
patch "/tournaments/#{tournament.id}/set_timer_end",
params: { timer_end_seconds: seconds },
headers: auth_headers_for(user),
as: :json
expect(response).to have_http_status(:ok)
parsed_response
end
def group_identifier_for(team)
team.name[/G(\d+)/, 1].to_i
end
def seed_identifier_for(team)
team.name[/S(\d+)/, 1].to_i
end
def playoff_rank_for(team)
[seed_identifier_for(team), group_identifier_for(team), team.name]
end
def preferred_winner_for(match)
match.teams.min_by { |team| playoff_rank_for(team) }
end
def play_match!(match, user:, winner: preferred_winner_for(match), winning_points: 10, losing_points: 1,
expected_finish_status: 200, expected_finish_error: nil)
patch "/matches/#{match.id}",
params: { state: 'in_progress' },
headers: auth_headers_for(user),
as: :json
expect(response.status).to eq(200), "starting match #{match.id} failed: #{response.body}"
ordered_scores = match.reload.match_scores.sort_by(&:id)
ordered_scores.each do |match_score|
points = match_score.team == winner ? winning_points : losing_points
patch "/match_scores/#{match_score.id}",
params: { points: },
headers: auth_headers_for(user),
as: :json
expect(response.status).to eq(200), "updating match_score #{match_score.id} failed: #{response.body}"
end
patch "/matches/#{match.id}",
params: { state: 'finished' },
headers: auth_headers_for(user),
as: :json
expect(response.status).to eq(
expected_finish_status
), "finishing match #{match.id} with teams #{match.reload.teams.map(&:name)} failed: #{response.body}"
return if expected_finish_error.nil?
expect(parsed_response[:error]).to eq(expected_finish_error)
end
def finish_group_stage_matches!(group_stage, user:)
group_stage.groups.order(:number).each do |group|
group.matches.sort_by(&:position).each do |match|
winner = match.teams.min_by { |team| [seed_identifier_for(team), team.name] }
play_match!(match, user:, winner:)
end
end
end
def finish_stage!(stage, user:)
patch "/stages/#{stage.id}",
params: { state: 'finished' },
headers: auth_headers_for(user),
as: :json
expect(response).to have_http_status(:ok)
end
def run_playoff_to_completion!(tournament, user:, include_final: true)
loop do
excluded_levels = [-1]
excluded_levels << 0 unless include_final
playable_matches = tournament.reload.stages
.reject { |stage| excluded_levels.include?(stage.level) }
.flat_map(&:matches)
.select(&:not_started?)
.sort_by { |match| [-match.stage.level, match.position] }
break if playable_matches.empty?
playable_matches.each do |match|
play_match!(match, user:)
end
end
tournament.reload
end
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
teams = group_count.times.flat_map do |group_index|
teams_per_group.times.map do |seed_index|
{
name: "G#{group_index + 1}-S#{seed_index + 1}",
group: group_index
}
end
end
{
name: "Group stage #{group_count}x#{teams_per_group}",
description: 'Lifecycle E2E tournament',
public: false,
group_stage: true,
playoff_teams_amount:,
teams:
}
end
def create_playoff_only_payload(team_count:)
{
name: "Playoff only #{team_count}",
description: 'Lifecycle E2E tournament',
public: false,
teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } }
}
end
let(:user) { create(:user) }
describe 'group stage lifecycle with four teams per group' do
it 'creates a 16-team tournament, finishes groups, generates playoffs, and reaches a champion' do
tournament = create_tournament!(
user:,
params: create_group_stage_payload(group_count: 4, teams_per_group: 4, playoff_teams_amount: 8)
)
expect(tournament.playoff_teams_amount).to eq(8)
expect(tournament.instant_finalists_amount).to eq(8)
expect(tournament.intermediate_round_participants_amount).to eq(0)
group_stage = tournament.stages.find_by(level: -1)
expect(group_stage).to be_present
expect(group_stage.groups.count).to eq(4)
expect(group_stage.groups.map(&:number)).to match_array([1, 2, 3, 4])
expect(group_stage.groups.map { |group| group.matches.count }).to all(eq(6))
get "/tournaments/#{tournament.id}/matches",
params: { state: 'upcoming' },
headers: { 'ACCEPT' => 'application/json' }
expect(response).to have_http_status(:ok)
expect(parsed_response.count).to eq(4)
expect(parsed_response.map { |match| match[:position] }).to all(eq(0))
timer_response = update_timer_by_seconds!(tournament, user, 600)
expect(Time.zone.parse(timer_response[:timer_end])).to be > Time.zone.now
get "/tournaments/#{tournament.id}/timer_end", headers: { 'ACCEPT' => 'application/json' }
expect(response).to have_http_status(:ok)
expect(Time.zone.parse(parsed_response[:timer_end])).to be > Time.zone.now
finish_group_stage_matches!(group_stage, user:)
get "/tournaments/#{tournament.id}/statistics", headers: { 'ACCEPT' => 'application/json' }
expect(response).to have_http_status(:ok)
expect(parsed_response[:group_scores].count).to eq(16)
expect(parsed_response[:most_dominant_score]).to be_present
expect(parsed_response[:least_dominant_score]).to be_present
advancing_teams_before_finish = tournament.reload.teams_advancing_from_group_stage
expect(advancing_teams_before_finish.map(&:name)).to eq(
%w[G1-S1 G3-S2 G2-S1 G4-S2 G3-S1 G1-S2 G4-S1 G2-S2]
)
finish_stage!(group_stage, user:)
tournament.reload
playoff_entry_stage = tournament.stages.where.not(level: -1).max_by(&:level)
expect(playoff_entry_stage.matches.count).to eq(4)
expect(playoff_entry_stage.matches.sort_by(&:position).map { |match| match.teams.map(&:name) }).to eq(
[
%w[G1-S1 G3-S2],
%w[G2-S1 G4-S2],
%w[G3-S1 G1-S2],
%w[G4-S1 G2-S2]
]
)
run_playoff_to_completion!(tournament, user:, include_final: false)
final_stage = tournament.stages.find_by(level: 0)
final_match = final_stage.matches.first
expect(final_match).to be_not_started
expect(final_match.teams.map(&:name)).to match_array(%w[G1-S1 G3-S1])
play_match!(
final_match,
user:,
expected_finish_status: 422,
expected_finish_error: 'Moving Team one stage down failed'
)
final_match.reload
expect(final_match).to be_in_progress
expect(final_match.current_leading_team.name).to eq('G1-S1')
end
it 'preserves the current intermediate-round behavior for three groups advancing into a four-team playoff' do
tournament = create_tournament!(
user:,
params: create_group_stage_payload(group_count: 3, teams_per_group: 4, playoff_teams_amount: 4)
)
expect(tournament.playoff_teams_amount).to eq(4)
expect(tournament.instant_finalists_amount).to eq(3)
expect(tournament.intermediate_round_participants_amount).to eq(2)
group_stage = tournament.stages.find_by(level: -1)
finish_group_stage_matches!(group_stage, user:)
finish_stage!(group_stage, user:)
tournament.reload
playoff_entry_stage = tournament.stages.where.not(level: -1).max_by(&:level)
expect(playoff_entry_stage).to be_intermediate_stage
expect(playoff_entry_stage.matches.count).to eq(4)
expect(playoff_entry_stage.matches.count(&:single_team?)).to eq(3)
expect(playoff_entry_stage.matches.count(&:not_started?)).to eq(1)
advancing_team_names = playoff_entry_stage.matches.flat_map { |match| match.teams.map(&:name) }
expect(advancing_team_names.uniq.count).to eq(5)
expect(advancing_team_names).to include('G1-S1', 'G2-S1', 'G3-S1')
run_playoff_to_completion!(tournament, user:, include_final: false)
final_match = tournament.stages.find_by(level: 0).matches.first
expect(final_match).to be_not_started
expect(final_match.teams.map(&:name)).to match_array(%w[G3-S1 G1-S1])
play_match!(
final_match,
user:,
expected_finish_status: 422,
expected_finish_error: 'Moving Team one stage down failed'
)
final_match.reload
expect(final_match).to be_in_progress
expect(final_match.current_leading_team.name).to eq('G1-S1')
end
end
describe 'playoff-only lifecycle across different team counts' do
[2, 3, 5, 8].each do |team_count|
it "plays a #{team_count}-team playoff tournament through to the end" do
tournament = create_tournament!(
user:,
params: create_playoff_only_payload(team_count:)
)
opening_stage = tournament.stages.max_by(&:level)
expect(opening_stage).to be_present
expect(opening_stage.matches).not_to be_empty
if Utils.po2?(team_count)
expect(opening_stage).to be_playoff_stage
else
expect(opening_stage).to be_intermediate_stage
expect(opening_stage.matches.count(&:single_team?)).to be > 0
end
run_playoff_to_completion!(tournament, user:)
tournament.reload
final_match = tournament.stages.find_by(level: 0).matches.first
expect(final_match).to be_finished
expect(final_match.winner.name).to eq('G0-S1')
end
end
end
end