turniere-backend/spec/services/tournament_snapshot_importe...

203 lines
7.7 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TournamentSnapshotImporter do
describe '.import!' do
it 'replaces follower tournament graph using source ids as stable mapping keys' do
source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
snapshot = TournamentSnapshotBuilder.build(source)
described_class.import!(tournament: follower, snapshot: snapshot)
follower.reload
expect(follower.sync_source_tournament_id).to eq(source.id)
expect(follower.name).to eq(source.name)
expect(follower.teams.count).to eq(snapshot.fetch(:teams).count)
expect(follower.stages.count).to eq(source.stages.count)
expect(follower.matches.count).to eq(source.matches.count)
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
end
it 'ignores stale snapshots that arrive after a newer one' do
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
newer_source = create(:tournament, name: 'Newer name')
older_source = create(:tournament, name: 'Older name')
newer_snapshot = TournamentSnapshotBuilder.build(newer_source).merge(sync_generated_at: '2026-04-14T10:30:02Z')
older_snapshot = TournamentSnapshotBuilder.build(older_source).merge(sync_generated_at: '2026-04-14T10:30:01Z')
described_class.import!(tournament: follower, snapshot: newer_snapshot)
described_class.import!(tournament: follower, snapshot: older_snapshot)
follower.reload
expect(follower.name).to eq('Newer name')
expect(follower.sync_last_imported_snapshot_at).to eq(Time.zone.parse('2026-04-14T10:30:02Z'))
end
it 'round-trips large leader tournament graph with mismatched ids, many groups, deciders and 64-team playoffs' do
create_list(:tournament, 3)
source = create_large_source_tournament!
create_list(:tournament, 5)
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
snapshot = TournamentSnapshotBuilder.build(source)
described_class.import!(tournament: follower, snapshot: snapshot)
follower.reload
expect(follower.id).not_to eq(source.id)
expect(sync_signature(follower)).to eq(sync_signature(source))
expect(follower.teams.count).to eq(128)
expect(follower.stages.where(level: -1).first.groups.count).to eq(32)
expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(63)
expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count)
expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id))
expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
end
end
def create_large_source_tournament!
tournament = create(
:tournament,
teams_count: 0,
name: 'Large Sync Source',
playoff_teams_amount: 64,
instant_finalists_amount: 64,
intermediate_round_participants_amount: 0
)
group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished)
advancing_teams = []
32.times do |group_index|
group = Group.create!(stage: group_stage, number: group_index + 1)
teams = 4.times.map do |seed_index|
create(
:team,
tournament: tournament,
name: "Leader-G#{group_index + 1}-S#{seed_index + 1}"
)
end
teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero?
teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15
create_group_round_robin!(group: group, teams: teams)
create_group_scores!(group: group, teams: teams)
if group_index < 3
create_decider_match!(group: group, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index])
end
advancing_teams.concat(teams.take(2))
end
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams)
expect(result).to be_a_success
tournament.reload
play_some_playoff_matches!(tournament)
tournament.reload
end
def create_group_round_robin!(group:, teams:)
pairings = [
[0, 1, 2, 1],
[2, 3, 3, 0],
[0, 2, 4, 1],
[1, 3, 2, 0],
[0, 3, 5, 2],
[1, 2, 4, 3]
]
pairings.each_with_index do |(left_index, right_index, left_points, right_points), position|
match = Match.create!(group: group, state: :finished, position: position, decider: false)
MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0)
MatchScore.create!(match: match, team: teams[right_index], points: right_points, hidden_points: 0)
end
end
def create_decider_match!(group:, teams:, hidden_points:)
match = Match.create!(group: group, state: :finished, position: 6, decider: true)
MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first)
MatchScore.create!(match: match, team: teams.second, points: 0, hidden_points: hidden_points.last)
end
def create_group_scores!(group:, teams:)
teams.each_with_index do |team, index|
GroupScore.create!(
group: group,
team: team,
group_points: 9 - index,
scored_points: 12 - index,
received_points: index,
position: index + 1
)
end
end
def play_some_playoff_matches!(tournament)
initial_stage = tournament.stages.where('level >= 0').max_by(&:level)
initial_stage.matches.sort_by(&:position).first(4).each_with_index do |match, index|
match.update!(state: :finished)
ordered_scores = match.match_scores.sort_by(&:id)
ordered_scores.first.update!(points: 20 + index)
ordered_scores.second.update!(points: index)
PlayoffStageService.populate_match_below(match).uniq.each(&:save!)
end
end
def sync_signature(tournament)
{
name: tournament.name,
public: tournament.public,
playoff_teams_amount: tournament.playoff_teams_amount,
instant_finalists_amount: tournament.instant_finalists_amount,
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
timer_end: tournament.timer_end&.iso8601,
teams: tournament.teams.order(:name).map { |team| team.name },
stages: tournament.stages.order(level: :asc).map do |stage|
{
level: stage.level,
state: stage.state,
groups: stage.groups.order(:number).map do |group|
{
number: group.number,
matches: group.matches.order(:position).map { |match| match_signature(match) },
group_scores: group.group_scores.includes(:team).sort_by(&:position).map do |score|
{
team_name: score.team.name,
position: score.position,
group_points: score.group_points,
scored_points: score.scored_points,
received_points: score.received_points
}
end
}
end,
matches: stage.matches.order(:position).map { |match| match_signature(match) }
}
end
}
end
def match_signature(match)
{
state: match.state,
position: match.position,
decider: match.decider,
scores: match.match_scores.includes(:team).sort_by { |score| score.team.name }.map do |score|
{
team_name: score.team.name,
points: score.points,
hidden_points: score.hidden_points
}
end
}
end
end