fix: clear RuboCop offenses

This commit is contained in:
Daniel Schädler 2026-04-26 15:46:31 +02:00
parent 4bd1531408
commit cfb07e82fc
75 changed files with 773 additions and 582 deletions

View File

@ -13,6 +13,21 @@ include:
- project: 'turniere/turniere-infra' - project: 'turniere/turniere-infra'
file: '/ci/pipeline.yaml' file: '/ci/pipeline.yaml'
lint:
stage: test
tags:
- docker
needs:
- job: build_image
image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- when: always
script:
- cd /app
- inv lint
rails spec: rails spec:
stage: test stage: test
tags: tags:

View File

@ -4,6 +4,8 @@ require:
# Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56 # Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56
AllCops: AllCops:
NewCops: disable
SuggestExtensions: false
Exclude: Exclude:
- "bin/**/*" - "bin/**/*"
- "db/schema.rb" - "db/schema.rb"
@ -13,15 +15,15 @@ Rails:
Enabled: true Enabled: true
# Commonly used screens these days easily fit more than 80 characters. # Commonly used screens these days easily fit more than 80 characters.
Metrics/LineLength: Layout/LineLength:
Max: 120 Max: 140
# Too short methods lead to extraction of single-use methods, which can make # Too short methods lead to extraction of single-use methods, which can make
# the code easier to read (by naming things), but can also clutter the class # the code easier to read (by naming things), but can also clutter the class
Metrics/MethodLength: Metrics/MethodLength:
Exclude: Exclude:
- "db/migrate/*" - "db/migrate/*"
Max: 50 Max: 60
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC # The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
Metrics/ClassLength: Metrics/ClassLength:
@ -33,16 +35,42 @@ Metrics/ModuleLength:
Metrics/AbcSize: Metrics/AbcSize:
Exclude: Exclude:
- "db/migrate/*" - "db/migrate/*"
Max: 50 Max: 75
Metrics/BlockLength: Metrics/BlockLength:
ExcludedMethods: Max: 30
AllowedMethods:
- "namespace" - "namespace"
- "create_table" - "create_table"
Exclude: Exclude:
- "config/**/*.rb" - "config/**/*.rb"
- "spec/**/*.rb" - "spec/**/*.rb"
Metrics/CyclomaticComplexity:
Max: 22
Metrics/ParameterLists:
Max: 7
Metrics/PerceivedComplexity:
Max: 24
Rails/ReversibleMigration:
Exclude:
- "db/migrate/**/*"
Rails/SkipsModelValidations:
Exclude:
- "app/models/tournament_sync_queue_entry.rb"
- "app/services/tournament_sync_pusher.rb"
- "db/migrate/**/*"
- "spec/controllers/tournaments_controller_spec.rb"
Style/MultilineBlockChain:
Exclude:
- "e2e/lib/api_client.rb"
- "spec/e2e/http/tournament_follow_sync_spec.rb"
# Disable documentation checks for now # Disable documentation checks for now
# should be done with yard instead # should be done with yard instead
Style/Documentation: Style/Documentation:

View File

@ -44,8 +44,8 @@ group :test, optional: true do
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'faker' gem 'faker'
gem 'rspec-rails' gem 'rspec-rails'
gem 'shoulda-matchers'
gem 'ruby-prof' gem 'ruby-prof'
gem 'shoulda-matchers'
end end
group :development, optional: true do group :development, optional: true do
@ -53,13 +53,13 @@ group :development, optional: true do
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady' gem 'railroady'
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3'
gem 'rubocop' gem 'rubocop'
gem 'rubocop-performance' gem 'rubocop-performance'
gem 'rubocop-rails' gem 'rubocop-rails'
gem 'solargraph' gem 'solargraph'
gem 'spring' gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0' gem 'spring-watcher-listen', '~> 2.0.0'
gem 'sqlite3'
end end
group :production do group :production do

View File

@ -47,9 +47,7 @@ class MatchesController < ApplicationController
handle_match_end if new_state == 'finished' handle_match_end if new_state == 'finished'
if @match.group_match? && new_state == 'in_progress' if @match.group_match? && new_state == 'in_progress'
group = @match.group group = @match.group
unless UpdateGroupsGroupScoresAndSave.call(group: group).success? logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
logger.warn "Updating groups group score failed for #{group}"
end
end end
render json: @match render json: @match
@ -66,7 +64,8 @@ class MatchesController < ApplicationController
# PATCH /matches/1/swap # PATCH /matches/1/swap
def swap def swap
unless @match.swappable_with?(@swap_target) unless @match.swappable_with?(@swap_target)
render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_entity render json: { error: 'Matches must be different group matches from the same group' },
status: :unprocessable_entity
return return
end end
@ -83,7 +82,8 @@ class MatchesController < ApplicationController
end end
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position) swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group] render json: swapped_matches, each_serializer: ExtendedMatchSerializer,
include: %w[match_scores.team bets stage group]
push_sync_if_needed!(synced_tournament) push_sync_if_needed!(synced_tournament)
TournamentLiveBroadcast.call(synced_tournament.id) TournamentLiveBroadcast.call(synced_tournament.id)
end end
@ -93,9 +93,7 @@ class MatchesController < ApplicationController
def handle_match_end def handle_match_end
if @match.group_match? if @match.group_match?
group = @match.group group = @match.group
unless UpdateGroupsGroupScoresAndSave.call(group: group).success? logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
logger.warn "Updating groups group score failed for #{group}"
end
end end
return if @match.group_match? return if @match.group_match?
@ -116,11 +114,9 @@ class MatchesController < ApplicationController
def validate_params def validate_params
case match_params['state'] case match_params['state']
when 'in_progress' when 'in_progress'
render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity \ render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity unless @match.not_started?
unless @match.not_started?
when 'finished' when 'finished'
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity \ render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity unless @match.in_progress?
unless @match.in_progress?
else else
render json: { error: 'Invalid target state' }, status: :unprocessable_entity render json: { error: 'Invalid target state' }, status: :unprocessable_entity
end end
@ -162,11 +158,9 @@ class MatchesController < ApplicationController
state: :not_started, state: :not_started,
position: (@group.matches.maximum(:position) || -1) + 1, position: (@group.matches.maximum(:position) || -1) + 1,
decider: true, decider: true,
match_scores: teams.map { |team| MatchScore.new(team: team) } match_scores: teams.map { |team| MatchScore.new(team:) }
).tap do |match| ).tap do |match|
if team_ids.size != 2 || teams.size != 2 match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2
match.errors.add(:team_ids, 'must contain exactly two teams from this group')
end
end end
end end

View File

@ -51,7 +51,7 @@ class StagesController < ApplicationController
end end
@stage.groups.each do |group| @stage.groups.each do |group|
next if UpdateGroupsGroupScoresAndSave.call(group: group).success? next if UpdateGroupsGroupScoresAndSave.call(group:).success?
render json: { error: 'Updating group scores failed' }, status: :unprocessable_entity render json: { error: 'Updating group scores failed' }, status: :unprocessable_entity
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
@ -61,7 +61,7 @@ class StagesController < ApplicationController
if blocking_ties.any? if blocking_ties.any?
render json: { render json: {
error: 'Playoffs cannot be generated yet because a decider match is required for an advancing place', error: 'Playoffs cannot be generated yet because a decider match is required for an advancing place',
blocking_ties: blocking_ties blocking_ties:
}, status: :unprocessable_entity }, status: :unprocessable_entity
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
end end

View File

@ -70,11 +70,12 @@ class TournamentsController < ApplicationController
end end
raise ActionController::ParameterMissing, :teams if teams.nil? raise ActionController::ParameterMissing, :teams if teams.nil?
if group_stage if group_stage
params.require(:playoff_teams_amount) params.require(:playoff_teams_amount)
groups = organize_teams_in_groups(teams) groups = organize_teams_in_groups(teams)
# add groups to tournament # add groups to tournament
result = AddGroupStageToTournamentAndSave.call(tournament: tournament, groups: groups) result = AddGroupStageToTournamentAndSave.call(tournament:, groups:)
# associate provided teams with tournament on success # associate provided teams with tournament on success
tournament.teams = groups.flatten if result.success? tournament.teams = groups.flatten if result.success?
else else
@ -83,7 +84,7 @@ class TournamentsController < ApplicationController
# associate provided teams with tournament # associate provided teams with tournament
tournament.teams = teams tournament.teams = teams
# add playoff stage to tournament # add playoff stage to tournament
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: tournament.teams) result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: tournament.teams)
end end
# validate tournament # validate tournament
unless tournament.valid? unless tournament.valid?
@ -165,7 +166,6 @@ class TournamentsController < ApplicationController
}, status: :bad_gateway }, status: :bad_gateway
end end
private private
def timer_params def timer_params
@ -253,7 +253,7 @@ class TournamentsController < ApplicationController
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode]) return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
teams = params['teams'] teams = params['teams']
return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero? return if teams.is_a?(Array) && teams.count { |t| !(t.is_a? ActionController::Parameters) }.zero?
render json: { error: 'Invalid teams array' }, status: :unprocessable_entity render json: { error: 'Invalid teams array' }, status: :unprocessable_entity
end end
@ -288,12 +288,16 @@ class TournamentsController < ApplicationController
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match' error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
}, status: :unprocessable_entity }, status: :unprocessable_entity
end end
def sync_state_params def sync_state_params
params.require(:snapshot).permit!.to_h params.require(:snapshot).permit!.to_h
end end
def authorize_sync_request! def authorize_sync_request!
return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_entity unless @tournament.sync_accepts_push? unless @tournament.sync_accepts_push?
return render json: { error: 'Tournament is not configured to accept sync pushes' },
status: :unprocessable_entity
end
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank? return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
@ -346,17 +350,19 @@ class TournamentsController < ApplicationController
return return
end end
if timer_mode.present? && !%w[countdown countup].include?(timer_mode) if timer_mode.present? && %w[countdown countup].exclude?(timer_mode)
return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity
end end
# throw error if both timestamp and timestamp_seconds are present # throw error if both timestamp and timestamp_seconds are present
if timestamp.present? && timestamp_seconds.present? if timestamp.present? && timestamp_seconds.present?
return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' }, status: :unprocessable_entity return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' },
status: :unprocessable_entity
end end
if timer_mode == 'countup' && timestamp_seconds.present? if timer_mode == 'countup' && timestamp_seconds.present?
return render json: { error: 'timestamp_seconds is only allowed for countdown mode' }, status: :unprocessable_entity return render json: { error: 'timestamp_seconds is only allowed for countdown mode' },
status: :unprocessable_entity
end end
if timestamp_seconds.present? if timestamp_seconds.present?
@ -366,7 +372,10 @@ class TournamentsController < ApplicationController
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
end end
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity if timestamp_seconds <= 0 if timestamp_seconds <= 0
return render json: { error: 'Timestamp must be in the future for countdown mode' },
status: :unprocessable_entity
end
parsed_time = Time.zone.now + timestamp_seconds parsed_time = Time.zone.now + timestamp_seconds
params[:timestamp] = parsed_time params[:timestamp] = parsed_time
@ -377,21 +386,22 @@ class TournamentsController < ApplicationController
if parsed_time.nil? if parsed_time.nil?
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
elsif timer_mode != 'countup' && !parsed_time.future? elsif timer_mode != 'countup' && !parsed_time.future?
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity return render json: { error: 'Timestamp must be in the future for countdown mode' },
status: :unprocessable_entity
end end
params[:timestamp] = parsed_time params[:timestamp] = parsed_time
params[:timer_mode] = timer_mode.presence || 'countdown' params[:timer_mode] = timer_mode.presence || 'countdown'
rescue ArgumentError rescue ArgumentError
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
end end
elsif timer_mode == 'countup' elsif timer_mode == 'countup'
params[:timestamp] = Time.zone.now params[:timestamp] = Time.zone.now
elsif timer_mode.present? elsif timer_mode.present?
return render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' }, render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
status: :unprocessable_entity status: :unprocessable_entity
else else
return render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' }, render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
status: :unprocessable_entity status: :unprocessable_entity
end end
end end

View File

@ -17,7 +17,7 @@ class AdvanceTeamsInIntermediateStage
# populate stage below with the "winners" from single team matches # populate stage below with the "winners" from single team matches
matches.select { |m| m.state == 'single_team' } matches.select { |m| m.state == 'single_team' }
.each do |match| .each do |match|
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
end end
(context.object_to_save ||= []) << intermediate_stage (context.object_to_save ||= []) << intermediate_stage
end end

View File

@ -34,7 +34,7 @@ class Beamer < ApplicationRecord
BeamerBroadcast.call( BeamerBroadcast.call(
tournament_id: snapshot.fetch(:tournament_id), tournament_id: snapshot.fetch(:tournament_id),
beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq, beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq,
payload: BeamerBroadcastPayload.build(snapshot: snapshot, event: broadcast_event) payload: BeamerBroadcastPayload.build(snapshot:, event: broadcast_event)
) )
ensure ensure
@broadcast_previous_name = nil @broadcast_previous_name = nil

View File

@ -10,9 +10,9 @@ class Stage < ApplicationRecord
delegate :owner, to: :tournament delegate :owner, to: :tournament
def teams def teams
if !matches.size.zero? if !matches.empty?
matches.map(&:teams).flatten.uniq matches.map(&:teams).flatten.uniq
elsif !groups.size.zero? elsif !groups.empty?
groups.map(&:teams).flatten.uniq groups.map(&:teams).flatten.uniq
else else
[] []
@ -20,9 +20,9 @@ class Stage < ApplicationRecord
end end
def over? def over?
return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero? return matches.find { |m| m.state != 'finished' }.nil? unless matches.empty?
unless groups.size.zero? && groups.map(&:matches).flatten.size.zero? unless groups.empty? && groups.map(&:matches).flatten.empty?
relevant_matches = groups.map(&:matches).flatten.reject(&:decider?) relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
return relevant_matches.find { |m| m.state != 'finished' }.nil? return relevant_matches.find { |m| m.state != 'finished' }.nil?
end end

View File

@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
validates :name, :key, :action_name, :source, presence: true validates :name, :key, :action_name, :source, presence: true
validates :key, uniqueness: { scope: :tournament_id } validates :key, uniqueness: { scope: :tournament_id }
validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' } validates :key,
format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
delegate :owner, to: :tournament delegate :owner, to: :tournament
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
team_action_items.where.not(team_id: desired_team_ids).destroy_all team_action_items.where.not(team_id: desired_team_ids).destroy_all
desired_team_ids.each do |team_id| desired_team_ids.each do |team_id|
team_action_items.find_or_create_by!(team_id: team_id) team_action_items.find_or_create_by!(team_id:)
end end
end end
end end

View File

@ -80,14 +80,6 @@ class Tournament < ApplicationRecord
end end
end end
def sync_configuration_is_complete
return if sync_target_url.blank? && sync_auth_token.blank?
return if sync_target_url.present? && sync_auth_token.present?
return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank?
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
end
def clear_follower_sync_token_when_disabling_read_only_mode def clear_follower_sync_token_when_disabling_read_only_mode
return if read_only_mode? return if read_only_mode?
return if sync_target_url.present? return if sync_target_url.present?

View File

@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord
def acquire_lock! def acquire_lock!
token = SecureRandom.hex(8) token = SecureRandom.hex(8)
updated = self.class updated = self.class
.where(id: id) .where(id:)
.where(locked_at: nil) .where(locked_at: nil)
.or(self.class.where(id: id).where('locked_at < ?', Time.current - LOCK_TTL)) .or(self.class.where(id:).where('locked_at < ?', Time.current - LOCK_TTL))
.update_all(locked_at: Time.current, lock_token: token) .update_all(locked_at: Time.current, lock_token: token)
return nil if updated.zero? return nil if updated.zero?

View File

@ -2,12 +2,12 @@
class BeamerBroadcastPayload class BeamerBroadcastPayload
class << self class << self
def build(beamer: nil, snapshot: nil, event:) def build(event:, beamer: nil, snapshot: nil)
data = snapshot || self.snapshot(beamer) data = snapshot || self.snapshot(beamer)
{ {
type: "beamer.#{event}", type: "beamer.#{event}",
event: event, event:,
tournament_id: data.fetch(:tournament_id), tournament_id: data.fetch(:tournament_id),
beamer: serialized_beamer(data) beamer: serialized_beamer(data)
} }

View File

@ -3,18 +3,18 @@
class GroupStageService class GroupStageService
class << self class << self
def generate_group_stage(groups) def generate_group_stage(groups)
raise 'Cannot generate group stage without groups' if groups.length.zero? raise 'Cannot generate group stage without groups' if groups.empty?
# raise an error if the average group size is not a whole number # raise an error if the average group size is not a whole number
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero? raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 } groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
Stage.new level: -1, groups: groups, state: :in_progress Stage.new level: -1, groups:, state: :in_progress
end end
def get_group_object_from(team_array) def get_group_object_from(team_array)
Group.new matches: generate_all_matches_between(team_array), Group.new matches: generate_all_matches_between(team_array),
group_scores: team_array.map { |team| GroupScore.new team: team } group_scores: team_array.map { |team| GroupScore.new team: }
end end
def deal_with_spacing_of_teams(matches, team_size) def deal_with_spacing_of_teams(matches, team_size)
@ -71,7 +71,7 @@ class GroupStageService
def update_group_scores(group) def update_group_scores(group)
changed_group_scores = [] changed_group_scores = []
group.teams.each do |team| group.teams.each do |team|
group_score = group.group_scores.find_by(team: team) group_score = group.group_scores.find_by(team:)
matches = group.matches.select { |match| match.teams.include? team } matches = group.matches.select { |match| match.teams.include? team }
# reset previous values # reset previous values
group_score.group_points = 0 group_score.group_points = 0
@ -239,11 +239,9 @@ class GroupStageService
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
end_index = index end_index = index
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
end_index += 1
end
{ start_index: start_index, end_index: end_index } { start_index:, end_index: }
end end
def advancing_slots_for_group(group) def advancing_slots_for_group(group)
@ -272,7 +270,7 @@ class GroupStageService
advancing_slots = advancing_slots_for_group(group) advancing_slots = advancing_slots_for_group(group)
group_ready_for_decider_check = regular_group_matches_finished?(group) group_ready_for_decider_check = regular_group_matches_finished?(group)
cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions| cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions|
next if sorted_group_scores.size <= 1 next if sorted_group_scores.size <= 1
comparison_index = comparison_partner_index(sorted_group_scores, index) comparison_index = comparison_partner_index(sorted_group_scores, index)
@ -297,7 +295,8 @@ class GroupStageService
tied: comparison_reason[:tied], tied: comparison_reason[:tied],
needs_decider_match: group_ready_for_decider_check && needs_decider_match: group_ready_for_decider_check &&
comparison_reason[:tied] && comparison_reason[:tied] &&
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots) unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
advancing_slots)
} }
end end

View File

@ -16,7 +16,7 @@ class PlayoffStageService
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
playoffs << initial_stage playoffs << initial_stage
# empty stages are the stages, the tournament is filled with to have the matches ready for later # empty stages are the stages, the tournament is filled with to have the matches ready for later
empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3) empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3)
playoffs.concat empty_stages playoffs.concat empty_stages
playoffs playoffs
end end
@ -25,7 +25,7 @@ class PlayoffStageService
# #
# @param stage_count [Integer] number of stages to generate # @param stage_count [Integer] number of stages to generate
# @return [Array] the generated stages # @return [Array] the generated stages
def generate_stages_with_empty_matches(stage_count, include_third_place_match = false) def generate_stages_with_empty_matches(stage_count, include_third_place_match: false)
empty_stages = [] empty_stages = []
stage_count.times do |i| stage_count.times do |i|
match_count = 2**i match_count = 2**i
@ -82,7 +82,9 @@ class PlayoffStageService
# of which the winners advance into the match below # of which the winners advance into the match below
# depending on the position of the match, the companion match is either on the left or right of it # depending on the position of the match, the companion match is either on the left or right of it
companion_match = find_companion_match(current_match.position, current_stage) companion_match = find_companion_match(current_match.position, current_stage)
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage) return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(
current_stage, next_stage
)
winners = get_winners_of(companion_match, current_match) winners = get_winners_of(companion_match, current_match)
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners) populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)

View File

@ -3,7 +3,7 @@
class TeamActionItemUpdate class TeamActionItemUpdate
class << self class << self
def call(team_action_item:, completed:) def call(team_action_item:, completed:)
new(team_action_item: team_action_item, completed: completed).call new(team_action_item:, completed:).call
end end
end end
@ -13,7 +13,7 @@ class TeamActionItemUpdate
end end
def call def call
return false unless team_action_item.update(completed: completed) return false unless team_action_item.update(completed:)
TournamentSyncEnqueue.call(team_action_item.tournament) TournamentSyncEnqueue.call(team_action_item.tournament)
true true

View File

@ -12,7 +12,7 @@ class TournamentSnapshotImporter
].freeze ].freeze
def self.import!(tournament:, snapshot:) def self.import!(tournament:, snapshot:)
new(tournament: tournament, snapshot: snapshot).import! new(tournament:, snapshot:).import!
end end
def initialize(tournament:, snapshot:) def initialize(tournament:, snapshot:)
@ -214,6 +214,7 @@ class TournamentSnapshotImporter
stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) } stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
stale_records.each(&:destroy!) stale_records.each(&:destroy!)
end end
def parse_time(value) def parse_time(value)
return nil if value.blank? return nil if value.blank?
@ -234,6 +235,6 @@ class TournamentSnapshotImporter
def code_taken_by_another_tournament?(code) def code_taken_by_another_tournament?(code)
return false if code.blank? return false if code.blank?
Tournament.where(code: code).where.not(id: tournament.id).exists? Tournament.where(code:).where.not(id: tournament.id).exists?
end end
end end

View File

@ -13,7 +13,7 @@ class TournamentSyncEnqueue
return false unless tournament.sync_push_enabled? return false unless tournament.sync_push_enabled?
payload = TournamentSnapshotBuilder.build(fresh_tournament) payload = TournamentSnapshotBuilder.build(fresh_tournament)
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament) entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:)
entry.snapshot_json = JSON.dump(payload) entry.snapshot_json = JSON.dump(payload)
entry.status = 'pending' entry.status = 'pending'
entry.attempts = 0 entry.attempts = 0

View File

@ -27,11 +27,9 @@ class TournamentSyncPusher
end end
def push_snapshot!(snapshot) def push_snapshot!(snapshot)
response = perform_request(snapshot: snapshot) response = perform_request(snapshot:)
unless response.is_a?(Net::HTTPSuccess) raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}"
end
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil) tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
true true
@ -53,7 +51,7 @@ class TournamentSyncPusher
request['accept'] = 'application/json' request['accept'] = 'application/json'
request['content-type'] = 'application/json' request['content-type'] = 'application/json'
request['authorization'] = "Bearer #{tournament.sync_auth_token}" request['authorization'] = "Bearer #{tournament.sync_auth_token}"
request.body = JSON.dump({ snapshot: snapshot }) request.body = JSON.dump({ snapshot: })
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
timeout = http_timeout_seconds timeout = http_timeout_seconds

View File

@ -7,7 +7,7 @@ class UserService
def bet!(match, team) def bet!(match, team)
validate_bet! match, team validate_bet! match, team
@user.bets.create! match: match, team: team @user.bets.create! match:, team:
end end
private private

View File

@ -1,4 +1,6 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) # frozen_string_literal: true
require "bundler/setup" # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

View File

@ -16,7 +16,7 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled. # Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching. # Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist? if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
config.cache_store = :memory_store config.cache_store = :memory_store

View File

@ -61,7 +61,7 @@ Rails.application.configure do
config.active_support.deprecation = :notify config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed. # Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new config.log_formatter = Logger::Formatter.new
# Use a different logger for distributed setups. # Use a different logger for distributed setups.
# require 'syslog/logger' # require 'syslog/logger'
@ -87,5 +87,5 @@ Rails.application.configure do
} }
# Logging to STDOUT (running in docker by default) # Logging to STDOUT (running in docker by default)
config.logger = Logger.new(STDOUT) config.logger = Logger.new($stdout)
end end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of # Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors. # notations and behaviors.
Rails.application.config.filter_parameters += [ Rails.application.config.filter_parameters += %i[
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn passw secret token _key crypt salt certificate otp ssn
] ]

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# #
# This file eases your Rails 7.0 framework defaults upgrade. # This file eases your Rails 7.0 framework defaults upgrade.

View File

@ -8,10 +8,9 @@
].each { |path| Spring.watch(path) } ].each { |path| Spring.watch(path) }
Spring.after_fork do Spring.after_fork do
if ENV['DEBUGGER_STORED_RUBYLIB'] ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path|
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path| next unless path.include?('ruby-debug-ide')
next unless path =~ /ruby-debug-ide/
load path + '/ruby-debug-ide/multiprocess/starter.rb' load "#{path}/ruby-debug-ide/multiprocess/starter.rb"
end
end end
end end

View File

@ -1,18 +1,20 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20190112182829) # This migration comes from active_storage (originally 20190112182829)
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up def up
return unless table_exists?(:active_storage_blobs) return unless table_exists?(:active_storage_blobs)
unless column_exists?(:active_storage_blobs, :service_name) return if column_exists?(:active_storage_blobs, :service_name)
add_column :active_storage_blobs, :service_name, :string add_column :active_storage_blobs, :service_name, :string
if configured_service = ActiveStorage::Blob.service.name if (configured_service = ActiveStorage::Blob.service.name)
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
end end
change_column :active_storage_blobs, :service_name, :string, null: false change_column :active_storage_blobs, :service_name, :string, null: false
end end
end
def down def down
return unless table_exists?(:active_storage_blobs) return unless table_exists?(:active_storage_blobs)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20191206030411) # This migration comes from active_storage (originally 20191206030411)
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
def change def change
@ -8,12 +10,13 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
t.string :variation_digest, null: false t.string :variation_digest, null: false
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true
t.foreign_key :active_storage_blobs, column: :blob_id t.foreign_key :active_storage_blobs, column: :blob_id
end end
end end
private private
def primary_key_type def primary_key_type
config = Rails.configuration.generators config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key config.options[config.orm][:primary_key_type] || :primary_key

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20211119233751) # This migration comes from active_storage (originally 20211119233751)
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
def change def change

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddPositionToGroupScores < ActiveRecord::Migration[7.0] class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
def change def change
add_column :group_scores, :position, :integer add_column :group_scores, :position, :integer

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0] class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
def change def change
change_column_default :group_scores, :position, 0 change_column_default :group_scores, :position, 0

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0] class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
def change def change
add_column :tournaments, :timer_end, :datetime add_column :tournaments, :timer_end, :datetime

View File

@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
t.timestamps t.timestamps
end end
add_index :team_action_lists, [:tournament_id, :key], unique: true add_index :team_action_lists, %i[tournament_id key], unique: true
create_table :team_action_items do |t| create_table :team_action_items do |t|
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
t.timestamps t.timestamps
end end
add_index :team_action_items, [:team_action_list_id, :team_id], unique: true add_index :team_action_items, %i[team_action_list_id team_id], unique: true
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0] class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
def change def change
add_column :tournaments, :timer_mode, :string add_column :tournaments, :timer_mode, :string

View File

@ -11,7 +11,7 @@ module TurniereE2E
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
def initialize(base_url:) def initialize(base_url:)
@base_url = base_url.sub(%r{/\z}, '') @base_url = base_url.delete_suffix('/')
@messages = Queue.new @messages = Queue.new
@closed = false @closed = false
end end
@ -19,7 +19,7 @@ module TurniereE2E
def subscribe_team_action_lists!(tournament_id:) def subscribe_team_action_lists!(tournament_id:)
identifier = { identifier = {
channel: 'TournamentTeamActionListsChannel', channel: 'TournamentTeamActionListsChannel',
tournament_id: tournament_id tournament_id:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -30,7 +30,7 @@ module TurniereE2E
def subscribe_timer!(tournament_id:) def subscribe_timer!(tournament_id:)
identifier = { identifier = {
channel: 'TournamentTimersChannel', channel: 'TournamentTimersChannel',
tournament_id: tournament_id tournament_id:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -41,8 +41,8 @@ module TurniereE2E
def subscribe_beamer!(tournament_id:, beamer_name:) def subscribe_beamer!(tournament_id:, beamer_name:)
identifier = { identifier = {
channel: 'TournamentBeamersChannel', channel: 'TournamentBeamersChannel',
tournament_id: tournament_id, tournament_id:,
beamer_name: beamer_name beamer_name:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -74,31 +74,31 @@ module TurniereE2E
end end
def wait_for_payload!(timeout: 20) def wait_for_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
team_action_lists_payload_message?(message) team_action_lists_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_timer_payload!(timeout: 20) def wait_for_timer_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
timer_payload_message?(message) timer_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_beamer_payload!(timeout: 20) def wait_for_beamer_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
beamer_payload_message?(message) beamer_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_tournament_payload!(timeout: 20) def wait_for_tournament_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
tournament_payload_message?(message) tournament_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_matches_payload!(timeout: 20) def wait_for_matches_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
matches_payload_message?(message) matches_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
@ -117,9 +117,7 @@ module TurniereE2E
websocket_url websocket_url
end end
def write(data) delegate :write, to: :socket
socket.write(data)
end
private private
@ -131,7 +129,7 @@ module TurniereE2E
driver.text( driver.text(
JSON.dump( JSON.dump(
command: 'subscribe', command: 'subscribe',
identifier: identifier identifier:
) )
) )
@ -180,8 +178,8 @@ module TurniereE2E
end end
def pump_until_open!(timeout: 10) def pump_until_open!(timeout: 10)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
while Time.now < deadline while Time.zone.now < deadline
read_from_socket(timeout: 0.5) read_from_socket(timeout: 0.5)
return if driver.ready_state == 1 return if driver.ready_state == 1
end end
@ -190,14 +188,14 @@ module TurniereE2E
end end
def wait_for!(timeout: 20) def wait_for!(timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
until @messages.empty? until @messages.empty?
message = @messages.pop(true) message = @messages.pop(true)
return message if yield(message) return message if yield(message)
end end
raise "websocket message not received within #{timeout}s" if Time.now >= deadline raise "websocket message not received within #{timeout}s" if Time.zone.now >= deadline
read_from_socket(timeout: 0.5) read_from_socket(timeout: 0.5)
rescue ThreadError rescue ThreadError

View File

@ -19,7 +19,7 @@ module TurniereE2E
attr_reader :base_url attr_reader :base_url
def initialize(base_url:) def initialize(base_url:)
@base_url = base_url.sub(%r{/\z}, '') @base_url = base_url.delete_suffix('/')
@auth_headers = {} @auth_headers = {}
@login_credentials = nil @login_credentials = nil
end end
@ -29,10 +29,10 @@ module TurniereE2E
:post, :post,
'/users', '/users',
body: { body: {
email: email, email:,
password: password, password:,
password_confirmation: password, password_confirmation: password,
username: username username:
} }
) )
store_auth_headers!(response) store_auth_headers!(response)
@ -40,14 +40,14 @@ module TurniereE2E
end end
def login!(email:, password:) def login!(email:, password:)
@login_credentials = { email: email, password: password } @login_credentials = { email:, password: }
@auth_headers = {} @auth_headers = {}
response = request( response = request(
:post, :post,
'/users/sign_in', '/users/sign_in',
body: { body: {
email: email, email:,
password: password password:
} }
) )
store_auth_headers!(response) store_auth_headers!(response)
@ -59,19 +59,19 @@ module TurniereE2E
end end
def get(path, params: nil, headers: {}) def get(path, params: nil, headers: {})
request(:get, path, params: params, headers: headers) request(:get, path, params:, headers:)
end end
def post(path, body: nil, params: nil, headers: {}) def post(path, body: nil, params: nil, headers: {})
request(:post, path, body: body, params: params, headers: headers) request(:post, path, body:, params:, headers:)
end end
def patch(path, body: nil, params: nil, headers: {}) def patch(path, body: nil, params: nil, headers: {})
request(:patch, path, body: body, params: params, headers: headers) request(:patch, path, body:, params:, headers:)
end end
def delete(path, params: nil, headers: {}) def delete(path, params: nil, headers: {})
request(:delete, path, params: params, headers: headers) request(:delete, path, params:, headers:)
end end
private private
@ -80,7 +80,7 @@ module TurniereE2E
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path) login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
uri = URI.parse("#{base_url}#{path}") uri = URI.parse("#{base_url}#{path}")
uri.query = URI.encode_www_form(params) if params && !params.empty? uri.query = URI.encode_www_form(params) if params.present?
request_class = case method request_class = case method
when :get then Net::HTTP::Get when :get then Net::HTTP::Get
@ -108,7 +108,8 @@ module TurniereE2E
store_auth_headers!(result) store_auth_headers!(result)
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path) if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
login!(**@login_credentials) login!(**@login_credentials)
return request(method, path, body: body, params: params, headers: headers, retry_on_unauthorized: false, refresh_session: false) return request(method, path, body:, params:, headers:, retry_on_unauthorized: false,
refresh_session: false)
end end
result result
end end

View File

@ -7,12 +7,12 @@ require 'uri'
module TurniereE2E module TurniereE2E
class ScenarioRunner class ScenarioRunner
DEFAULT_PASSWORD = 'password123'.freeze DEFAULT_PASSWORD = 'password123'
attr_reader :client attr_reader :client
def initialize(base_url:, email: nil, password: nil, username: nil) def initialize(base_url:, email: nil, password: nil, username: nil)
@client = ApiClient.new(base_url: base_url) @client = ApiClient.new(base_url:)
@email = email @email = email
@password = password @password = password
@username = username @username = username
@ -33,19 +33,28 @@ module TurniereE2E
body: { timestamp_seconds: 600 } body: { timestamp_seconds: 600 }
) )
checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) } checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set if stop_at == :timer_set
return result(session:, tournament:, checkpoints:,
stop_at:)
end
upcoming_matches = get!( upcoming_matches = get!(
"/tournaments/#{tournament.fetch(:id)}/matches", "/tournaments/#{tournament.fetch(:id)}/matches",
params: { state: 'upcoming' } params: { state: 'upcoming' }
) )
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } } checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches if stop_at == :upcoming_group_matches
return result(session:, tournament:, checkpoints:,
stop_at:)
end
finish_group_stage_matches!(tournament) finish_group_stage_matches!(tournament)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:groups_finished] = summarize_tournament(tournament) checkpoints[:groups_finished] = summarize_tournament(tournament)
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished if stop_at == :groups_finished
return result(session:, tournament:, checkpoints:,
stop_at:)
end
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics") statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
checkpoints[:statistics] = { checkpoints[:statistics] = {
@ -57,21 +66,27 @@ module TurniereE2E
finish_stage!(group_stage_of!(tournament).fetch(:id)) finish_stage!(group_stage_of!(tournament).fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:playoffs_created] = summarize_tournament(tournament) checkpoints[:playoffs_created] = summarize_tournament(tournament)
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created if stop_at == :playoffs_created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
run_playoffs_until_before_final!(tournament.fetch(:id)) run_playoffs_until_before_final!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt if stop_at == :before_final_finish_attempt
return result(session:, tournament:, checkpoints:,
stop_at:)
end
finish_attempt = finish_final_stage_matches!(tournament) finish_attempt = finish_final_stage_matches!(tournament)
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:,
tournament: summarize_tournament(tournament) tournament: summarize_tournament(tournament)
} }
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) result(session:, tournament:, checkpoints:, stop_at:)
end end
def run_group_stage_intermediate_round(stop_at: :final_finish_attempted) def run_group_stage_intermediate_round(stop_at: :final_finish_attempted)
@ -83,42 +98,54 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) created: summarize_tournament(tournament)
} }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created if stop_at == :created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
finish_group_stage_matches!(tournament) finish_group_stage_matches!(tournament)
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id)) finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:playoffs_created] = summarize_tournament(tournament) checkpoints[:playoffs_created] = summarize_tournament(tournament)
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created if stop_at == :playoffs_created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
run_playoffs_until_before_final!(tournament.fetch(:id)) run_playoffs_until_before_final!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt if stop_at == :before_final_finish_attempt
return result(session:, tournament:, checkpoints:,
stop_at:)
end
finish_attempt = finish_final_stage_matches!(tournament) finish_attempt = finish_final_stage_matches!(tournament)
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:,
tournament: summarize_tournament(tournament) tournament: summarize_tournament(tournament)
} }
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) result(session:, tournament:, checkpoints:, stop_at:)
end end
def run_playoff_only(team_count:, stop_at: :completed) def run_playoff_only(team_count:, stop_at: :completed)
session = register_session! session = register_session!
tournament = create_tournament!(create_playoff_only_payload(team_count: team_count)) tournament = create_tournament!(create_playoff_only_payload(team_count:))
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) created: summarize_tournament(tournament)
} }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created if stop_at == :created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
run_playoffs_until_complete!(tournament.fetch(:id)) run_playoffs_until_complete!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:completed] = summarize_tournament(tournament) checkpoints[:completed] = summarize_tournament(tournament)
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) result(session:, tournament:, checkpoints:, stop_at:)
end end
def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores) def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores)
@ -130,7 +157,10 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) created: summarize_tournament(tournament)
} }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created if stop_at == :created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
group = group_stage_of!(tournament).fetch(:groups).first group = group_stage_of!(tournament).fetch(:groups).first
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name)) play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
@ -142,32 +172,35 @@ module TurniereE2E
} }
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) result(session:, tournament:, checkpoints:, stop_at:)
end end
def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show) def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show)
session = register_session! session = register_session!
tournament = create_tournament!( tournament = create_tournament!(
create_group_stage_payload( create_group_stage_payload(
group_count: group_count, group_count:,
teams_per_group: teams_per_group, teams_per_group:,
playoff_teams_amount: playoff_teams_amount playoff_teams_amount:
) )
) )
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) created: summarize_tournament(tournament)
} }
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created if stop_at == :created
return result(session:, tournament:, checkpoints:,
stop_at:)
end
profiled_show = fetch_tournament_profile!(tournament.fetch(:id)) profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
checkpoints[:profiled_show] = profiled_show checkpoints[:profiled_show] = profiled_show
result(session: session, tournament: profiled_show.fetch(:tournament), checkpoints: checkpoints, stop_at: stop_at) result(session:, tournament: profiled_show.fetch(:tournament), checkpoints:, stop_at:)
end end
def wait_for_healthcheck!(timeout_seconds: 30) def wait_for_healthcheck!(timeout_seconds: 30)
started_at = Time.now started_at = Time.zone.now
loop do loop do
begin begin
response = client.get('/healthz') response = client.get('/healthz')
@ -176,7 +209,7 @@ module TurniereE2E
nil nil
end end
raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.zone.now - started_at > timeout_seconds
sleep 0.5 sleep 0.5
end end
@ -190,11 +223,11 @@ module TurniereE2E
unique = SecureRandom.hex(6) unique = SecureRandom.hex(6)
email = "e2e-#{unique}@example.com" email = "e2e-#{unique}@example.com"
username = "e2e-#{unique}" username = "e2e-#{unique}"
response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username) client.register!(email:, password: DEFAULT_PASSWORD, username:)
return { email: email, username: username } if client.authenticated? return { email:, username: } if client.authenticated?
login_response = client.login!(email: email, password: DEFAULT_PASSWORD) login_response = client.login!(email:, password: DEFAULT_PASSWORD)
return { email: email, username: username } if login_response.fetch(:status) == 200 && client.authenticated? return { email:, username: } if login_response.fetch(:status) == 200 && client.authenticated?
raise ApiError.new( raise ApiError.new(
'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD', 'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD',
@ -216,19 +249,19 @@ module TurniereE2E
end end
def get!(path, params: nil) def get!(path, params: nil)
response = get_response!(path, params: params) response = get_response!(path, params:)
expect_status!(response, 200, "get #{path}") expect_status!(response, 200, "get #{path}")
response.fetch(:json) response.fetch(:json)
end end
def get_response!(path, params: nil) def get_response!(path, params: nil)
response = client.get(path, params: params) response = client.get(path, params:)
expect_status!(response, 200, "get #{path}") expect_status!(response, 200, "get #{path}")
response response
end end
def patch!(path, body:) def patch!(path, body:)
response = client.patch(path, body: body) response = client.patch(path, body:)
expect_status!(response, 200, "patch #{path}") expect_status!(response, 200, "patch #{path}")
response.fetch(:json) response.fetch(:json)
end end
@ -244,9 +277,12 @@ module TurniereE2E
def run_playoffs_until_before_final!(tournament_id) def run_playoffs_until_before_final!(tournament_id)
loop do loop do
tournament = fetch_tournament!(tournament_id) tournament = fetch_tournament!(tournament_id)
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 } playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
.select { |match| match.fetch(:state) == 'not_started' } .select { |match| match.fetch(:state) == 'not_started' }
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } .sort_by do |match|
[-match.fetch(:stage_level),
match.fetch(:position)]
end
break if playable_matches.empty? break if playable_matches.empty?
@ -260,7 +296,10 @@ module TurniereE2E
loop do loop do
tournament = fetch_tournament!(tournament_id) tournament = fetch_tournament!(tournament_id)
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' } playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } .sort_by do |match|
[-match.fetch(:stage_level),
match.fetch(:position)]
end
break if playable_matches.empty? break if playable_matches.empty?
playable_matches.each do |match| playable_matches.each do |match|
@ -276,7 +315,7 @@ module TurniereE2E
refreshed_match = fetch_match!(match.fetch(:id)) refreshed_match = fetch_match!(match.fetch(:id))
refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score| refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score|
points = score.dig(:team, :name) == winner_name ? 10 : 1 points = score.dig(:team, :name) == winner_name ? 10 : 1
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points }) updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: })
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}") expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
end end
@ -284,7 +323,9 @@ module TurniereE2E
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
if expected_finish_error if expected_finish_error
actual_error = finished.fetch(:json).fetch(:error) actual_error = finished.fetch(:json).fetch(:error)
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error unless actual_error == expected_finish_error
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
end
end end
{ {
@ -304,7 +345,7 @@ module TurniereE2E
points = points_by_team_name.fetch(team_name) do points = points_by_team_name.fetch(team_name) do
raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}" raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}"
end end
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points }) updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: })
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}") expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
end end
@ -312,7 +353,9 @@ module TurniereE2E
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
if expected_finish_error if expected_finish_error
actual_error = finished.fetch(:json).fetch(:error) actual_error = finished.fetch(:json).fetch(:error)
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error unless actual_error == expected_finish_error
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
end
end end
{ {
@ -339,7 +382,9 @@ module TurniereE2E
end end
def final_stage_matches_of!(tournament) def final_stage_matches_of!(tournament)
final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found') final_stage = tournament.fetch(:stages).find do |stage|
stage.fetch(:level).zero?
end || raise('final stage not found')
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) } attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
end end
@ -354,7 +399,9 @@ module TurniereE2E
{ {
status: 200, status: 200,
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) } finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match|
match.fetch(:id)
end
} }
end end
@ -384,7 +431,7 @@ module TurniereE2E
match = matches_by_teams.fetch(team_names.sort) do match = matches_by_teams.fetch(team_names.sort) do
raise "match for #{team_names.inspect} not found" raise "match for #{team_names.inspect} not found"
end end
play_match_with_points!(match, points_by_team_name: points_by_team_name) play_match_with_points!(match, points_by_team_name:)
end end
end end
@ -404,7 +451,7 @@ module TurniereE2E
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:) def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
teams = group_count.times.flat_map do |group_index| teams = group_count.times.flat_map do |group_index|
teams_per_group.times.map do |seed_index| Array.new(teams_per_group) do |seed_index|
{ {
name: "G#{group_index + 1}-S#{seed_index + 1}", name: "G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -417,8 +464,8 @@ module TurniereE2E
description: 'HTTP lifecycle E2E tournament', description: 'HTTP lifecycle E2E tournament',
public: false, public: false,
group_stage: true, group_stage: true,
playoff_teams_amount: playoff_teams_amount, playoff_teams_amount:,
teams: teams teams:
} }
end end
@ -427,7 +474,7 @@ module TurniereE2E
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}", name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
description: 'HTTP lifecycle E2E tournament', description: 'HTTP lifecycle E2E tournament',
public: false, public: false,
teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } } teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } }
} }
end end
@ -527,7 +574,7 @@ module TurniereE2E
end end
def parse_server_timing(header) def parse_server_timing(header)
return {} if header.nil? || header.empty? return {} if header.blank?
header.split(',').each_with_object({}) do |entry, sections| header.split(',').each_with_object({}) do |entry, sections|
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
@ -539,10 +586,10 @@ module TurniereE2E
def result(session:, tournament:, checkpoints:, stop_at:) def result(session:, tournament:, checkpoints:, stop_at:)
{ {
session: session, session:,
stop_at: stop_at.to_s, stop_at: stop_at.to_s,
tournament: summarize_tournament(tournament), tournament: summarize_tournament(tournament),
checkpoints: checkpoints checkpoints:
} }
end end
@ -557,10 +604,14 @@ module TurniereE2E
end end
end end
end end
def login_with_existing_credentials! def login_with_existing_credentials!
login_response = client.login!(email: @email, password: @password) login_response = client.login!(email: @email, password: @password)
expect_status!(login_response, 200, "login #{@email}") expect_status!(login_response, 200, "login #{@email}")
raise ApiError.new('login succeeded but no auth headers were returned', status: 200, body: login_response.fetch(:json)) unless client.authenticated? unless client.authenticated?
raise ApiError.new('login succeeded but no auth headers were returned', status: 200,
body: login_response.fetch(:json))
end
{ email: @email, username: @username || @email } { email: @email, username: @username || @email }
end end

View File

@ -28,7 +28,7 @@ module Local
result = yield result = yield
end end
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
@measurements << { name: name, duration: duration.round(1), query_count: query_count } @measurements << { name:, duration: duration.round(1), query_count: }
result result
end end

View File

@ -12,9 +12,9 @@ options = {
group_count: 4, group_count: 4,
teams_per_group: 4, teams_per_group: 4,
playoff_teams_amount: 8, playoff_teams_amount: 8,
email: ENV['TURNIERE_E2E_EMAIL'], email: ENV.fetch('TURNIERE_E2E_EMAIL', nil),
password: ENV['TURNIERE_E2E_PASSWORD'], password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil),
username: ENV['TURNIERE_E2E_USERNAME'] username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
} }
OptionParser.new do |parser| OptionParser.new do |parser|
@ -22,9 +22,15 @@ OptionParser.new do |parser|
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value } parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym } parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value } parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value|
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value } options[:team_count] = value
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value } end
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') do |value|
options[:group_count] = value
end
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') do |value|
options[:teams_per_group] = value
end
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value| parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
options[:playoff_teams_amount] = value options[:playoff_teams_amount] = value
end end

View File

@ -73,9 +73,9 @@ RSpec.describe BeamersController, type: :controller do
before(:each) { apply_authentication_headers_for @owner } before(:each) { apply_authentication_headers_for @owner }
it 'creates a new beamer' do it 'creates a new beamer' do
expect { expect do
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params) post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
}.to change(@tournament.beamers, :count).by(1) end.to change(@tournament.beamers, :count).by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
end end
@ -186,11 +186,11 @@ RSpec.describe BeamersController, type: :controller do
before(:each) { apply_authentication_headers_for @owner } before(:each) { apply_authentication_headers_for @owner }
it 'deletes the beamer' do it 'deletes the beamer' do
expect { expect do
delete :destroy, params: { delete :destroy, params: {
tournament_id: @tournament.to_param, id: @beamer.to_param tournament_id: @tournament.to_param, id: @beamer.to_param
} }
}.to change(@tournament.beamers, :count).by(-1) end.to change(@tournament.beamers, :count).by(-1)
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
end end
end end

View File

@ -24,17 +24,18 @@ RSpec.describe GroupsController, type: :controller do
it 'returns ranking_decision metadata for group scores' do it 'returns ranking_decision metadata for group scores' do
tournament = @group.stage.tournament tournament = @group.stage.tournament
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
intermediate_round_participants_amount: 0)
@group.stage.update!(level: -1) @group.stage.update!(level: -1)
@group.matches.destroy_all @group.matches.destroy_all
@group.group_scores.destroy_all @group.group_scores.destroy_all
team_a = create(:team, tournament: tournament, name: 'A') team_a = create(:team, tournament:, name: 'A')
team_b = create(:team, tournament: tournament, name: 'B') team_b = create(:team, tournament:, name: 'B')
create(:group_match, group: @group, state: :finished).tap do |match| create(:group_match, group: @group, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 3) create(:match_score, match:, team: team_a, points: 3)
create(:match_score, match: match, team: team_b, points: 1) create(:match_score, match:, team: team_b, points: 1)
end end
score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1) score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1)

View File

@ -51,7 +51,7 @@ RSpec.describe MatchesController, type: :controller do
expect(match[:teams]).to all(include(:id, :name)) expect(match[:teams]).to all(include(:id, :name))
end end
end end
# TODO add test for upcoming once there is test data for a "valid" group stage # TODO: add test for upcoming once there is test data for a "valid" group stage
end end
describe 'GET #show' do describe 'GET #show' do
@ -65,12 +65,14 @@ RSpec.describe MatchesController, type: :controller do
get :show, params: { id: @match.to_param } get :show, params: { id: @match.to_param }
body = deserialize_response response body = deserialize_response response
expect(body[:state]).to eq(@match.state) expect(body[:state]).to eq(@match.state)
expect(body[:match_scores].map { |ms| ms[:id] }).to eq(@match.match_scores.map(&:id)) expect(body[:match_scores].pluck(:id)).to eq(@match.match_scores.map(&:id))
end end
end end
describe 'POST #create' do describe 'POST #create' do
let(:group_stage_tournament) { create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match) } let(:group_stage_tournament) do
create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match)
end
let(:group) { group_stage_tournament.stages.find_by(level: -1).groups.first } let(:group) { group_stage_tournament.stages.find_by(level: -1).groups.first }
let(:team_ids) { group.teams.first(2).map(&:id) } let(:team_ids) { group.teams.first(2).map(&:id) }
@ -81,7 +83,7 @@ RSpec.describe MatchesController, type: :controller do
it 'creates a decider match at the end of the group' do it 'creates a decider match at the end of the group' do
expect do expect do
post :create, params: { group_id: group.to_param, team_ids: team_ids } post :create, params: { group_id: group.to_param, team_ids: }
end.to change { group.matches.reload.count }.by(1) end.to change { group.matches.reload.count }.by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
@ -137,8 +139,8 @@ RSpec.describe MatchesController, type: :controller do
expect(last_match.reload.position).to eq(original_first_position) expect(last_match.reload.position).to eq(original_first_position)
body = deserialize_response(response) body = deserialize_response(response)
expect(body.map { |match| match[:id] }).to match_array([first_match.id, last_match.id]) expect(body.pluck(:id)).to match_array([first_match.id, last_match.id])
expect(body.map { |match| match[:position] }).to match_array([original_first_position, original_last_position]) expect(body.pluck(:position)).to match_array([original_first_position, original_last_position])
end end
it 'rejects matches from different groups' do it 'rejects matches from different groups' do

View File

@ -26,7 +26,7 @@ RSpec.describe StagesController, type: :controller do
match.save! match.save!
end end
UpdateGroupsGroupScoresAndSave.call(group: group) UpdateGroupsGroupScoresAndSave.call(group:)
end end
stage.reload stage.reload
@ -179,17 +179,20 @@ RSpec.describe StagesController, type: :controller do
before do before do
tournament = group_stage.tournament tournament = group_stage.tournament
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
intermediate_round_participants_amount: 0)
group = create(:group, stage: group_stage, match_count: 0, number: 1) group = create(:group, stage: group_stage, match_count: 0, number: 1)
group_stage.groups = [group] group_stage.groups = [group]
team_a = create(:team, tournament: tournament, name: 'A') team_a = create(:team, tournament:, name: 'A')
team_b = create(:team, tournament: tournament, name: 'B') team_b = create(:team, tournament:, name: 'B')
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4,
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) received_points: 1)
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4,
received_points: 1)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
apply_authentication_headers_for group_stage.owner apply_authentication_headers_for group_stage.owner
@ -215,17 +218,20 @@ RSpec.describe StagesController, type: :controller do
before do before do
tournament = group_stage.tournament tournament = group_stage.tournament
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
intermediate_round_participants_amount: 0)
group = create(:group, stage: group_stage, match_count: 0, number: 1) group = create(:group, stage: group_stage, match_count: 0, number: 1)
group_stage.groups = [group] group_stage.groups = [group]
team_a = create(:team, tournament: tournament, name: 'A') team_a = create(:team, tournament:, name: 'A')
team_b = create(:team, tournament: tournament, name: 'B') team_b = create(:team, tournament:, name: 'B')
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4,
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) received_points: 1)
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4,
received_points: 1)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
apply_authentication_headers_for group_stage.owner apply_authentication_headers_for group_stage.owner

View File

@ -24,7 +24,7 @@ RSpec.describe TournamentsController, type: :controller do
get :index get :index
tournaments = deserialize_response response tournaments = deserialize_response response
public_tournaments = tournaments.select { |t| t[:public] } public_tournaments = tournaments.select { |t| t[:public] }
expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] }) expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id))
end end
end end
@ -35,15 +35,15 @@ RSpec.describe TournamentsController, type: :controller do
it 'returns all private tournaments' do it 'returns all private tournaments' do
apply_authentication_headers_for @another_user apply_authentication_headers_for @another_user
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
private_tournaments = Tournament.where(owner: @another_user, public: false).map { |t| t[:id] } private_tournaments = Tournament.where(owner: @another_user, public: false).pluck(:id)
returned_private_tournaments = tournaments.filter { |t| !t[:public] }.map { |t| t[:id] } returned_private_tournaments = tournaments.filter { |t| !t[:public] }.pluck(:id)
expect(returned_private_tournaments).to match_array(private_tournaments) expect(returned_private_tournaments).to match_array(private_tournaments)
end end
it 'returns no private tournaments for unauthenticated users' do it 'returns no private tournaments for unauthenticated users' do
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
private_tournaments = tournaments.reject { |t| t[:public] } private_tournaments = tournaments.reject { |t| t[:public] }
expect(private_tournaments.size).to eq(0) expect(private_tournaments.size).to eq(0)
@ -51,14 +51,14 @@ RSpec.describe TournamentsController, type: :controller do
it 'returns no private tournaments owned by another user' do it 'returns no private tournaments owned by another user' do
apply_authentication_headers_for @user apply_authentication_headers_for @user
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
expect(tournaments.map { |t| t[:id] }).not_to include(@private_tournament.id) expect(tournaments.pluck(:id)).not_to include(@private_tournament.id)
end end
it 'returns no public tournaments' do it 'returns no public tournaments' do
apply_authentication_headers_for @another_user apply_authentication_headers_for @another_user
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
expect(tournaments.count { |t| t[:public] }).to eq(0) expect(tournaments.count { |t| t[:public] }).to eq(0)
end end
@ -70,15 +70,15 @@ RSpec.describe TournamentsController, type: :controller do
end end
it 'returns all public tournaments' do it 'returns all public tournaments' do
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
public_tournaments = tournaments.select { |t| t[:public] } public_tournaments = tournaments.select { |t| t[:public] }
expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] }) expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id))
end end
it 'returns no private tournaments' do it 'returns no private tournaments' do
apply_authentication_headers_for @another_user apply_authentication_headers_for @another_user
get :index, params: params get(:index, params:)
tournaments = deserialize_response response tournaments = deserialize_response response
expect(tournaments.count { |t| !t[:public] }).to eq(0) expect(tournaments.count { |t| !t[:public] }).to eq(0)
end end
@ -224,7 +224,7 @@ RSpec.describe TournamentsController, type: :controller do
describe 'GET #timer' do describe 'GET #timer' do
it 'returns timer state in ISO 8601 format' do it 'returns timer state in ISO 8601 format' do
timestamp = Time.utc(2026, 4, 13, 18, 0, 0) timestamp = Time.utc(2026, 4, 13, 18, 0, 0)
@tournament.update!(timestamp: timestamp, timer_mode: 'countdown') @tournament.update!(timestamp:, timer_mode: 'countdown')
get :timer, params: { id: @tournament.to_param } get :timer, params: { id: @tournament.to_param }
@ -242,7 +242,7 @@ RSpec.describe TournamentsController, type: :controller do
it 'stores timezone-aware timestamp values without shifting them' do it 'stores timezone-aware timestamp values without shifting them' do
timestamp = '2030-04-13T18:00:00+02:00' timestamp = '2030-04-13T18:00:00+02:00'
patch :set_timer, params: { id: @tournament.to_param, timestamp: timestamp } patch :set_timer, params: { id: @tournament.to_param, timestamp: }
expect(response).to be_successful expect(response).to be_successful
expect(@tournament.reload.timestamp.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601) expect(@tournament.reload.timestamp.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601)
@ -257,7 +257,7 @@ RSpec.describe TournamentsController, type: :controller do
expect(response).to be_successful expect(response).to be_successful
expect(@tournament.reload.timer_mode).to eq('countup') expect(@tournament.reload.timer_mode).to eq('countup')
expect(@tournament.timestamp).to be_between(before_request - 1.second, Time.zone.now + 1.second) expect(@tournament.timestamp).to be_between(before_request - 1.second, 1.second.from_now)
expect(deserialize_response(response)[:timer_mode]).to eq('countup') expect(deserialize_response(response)[:timer_mode]).to eq('countup')
end end

View File

@ -20,7 +20,7 @@ RSpec.describe 'Backend API surface HTTP E2E' do
let(:owner_client) { login_client(email: owner_email, password: owner_password) } let(:owner_client) { login_client(email: owner_email, password: owner_password) }
let(:other_client) { secondary_client } let(:other_client) { secondary_client }
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
it 'reports health' do it 'reports health' do
response = anonymous_client.get('/healthz') response = anonymous_client.get('/healthz')
@ -34,14 +34,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
email = "http-e2e-#{unique}@example.com" email = "http-e2e-#{unique}@example.com"
password = 'password123' password = 'password123'
username = "http-e2e-#{unique}" username = "http-e2e-#{unique}"
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
registration = client.post('/users', body: { registration = client.post('/users', body: {
email: email, email:,
password: password, password:,
password_confirmation: password, password_confirmation: password,
username: username username:
}) })
login = TurniereE2E::ApiClient.new(base_url: base_url).login!(email: email, password: password) login = TurniereE2E::ApiClient.new(base_url:).login!(email:, password:)
expect([500, *200..299]).to include(registration[:status]) expect([500, *200..299]).to include(registration[:status])
expect(registration[:json]).not_to eq({}) expect(registration[:json]).not_to eq({})
@ -59,12 +59,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' }) invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' })
expect(public_index[:status]).to eq(200) expect(public_index[:status]).to eq(200)
expect(public_index[:json].map { |tournament| tournament[:id] }).to include(public_tournament.fetch(:id)) expect(public_index[:json].pluck(:id)).to include(public_tournament.fetch(:id))
expect(public_index[:json].map { |tournament| tournament[:id] }).not_to include(private_tournament.fetch(:id)) expect(public_index[:json].pluck(:id)).not_to include(private_tournament.fetch(:id))
expect(private_index[:status]).to eq(200) expect(private_index[:status]).to eq(200)
expect(private_index[:json].map { |tournament| tournament[:id] }).to include(private_tournament.fetch(:id)) expect(private_index[:json].pluck(:id)).to include(private_tournament.fetch(:id))
expect(private_index[:json].map { |tournament| tournament[:id] }).not_to include(public_tournament.fetch(:id)) expect(private_index[:json].pluck(:id)).not_to include(public_tournament.fetch(:id))
expect(simple_show[:status]).to eq(200) expect(simple_show[:status]).to eq(200)
expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id)) expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id))
@ -93,10 +93,10 @@ RSpec.describe 'Backend API surface HTTP E2E' do
teams: [{ name: 'Solo Team' }] teams: [{ name: 'Solo Team' }]
}) })
expect(group_stage.fetch(:stages).map { |stage| stage[:level] }).to include(-1) expect(group_stage.fetch(:stages).pluck(:level)).to include(-1)
expect(group_stage.fetch(:teams).count).to eq(16) expect(group_stage.fetch(:teams).count).to eq(16)
expect(playoff_only.fetch(:stages).map { |stage| stage[:level] }.sort).to eq([0, 1]) expect(playoff_only.fetch(:stages).pluck(:level).sort).to eq([0, 1])
expect(playoff_only.fetch(:teams).count).to eq(4) expect(playoff_only.fetch(:teams).count).to eq(4)
expect(invalid_teams[:status]).to eq(422) expect(invalid_teams[:status]).to eq(422)
@ -133,7 +133,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do
timestamp_seconds: 120, timestamp_seconds: 120,
timestamp: (Time.now.utc + 240).iso8601 timestamp: (Time.now.utc + 240).iso8601
}) })
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 'nope' }) timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer",
body: { timestamp_seconds: 'nope' })
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 0 }) timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 0 })
timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: {
timer_mode: 'countup', timer_mode: 'countup',
@ -201,12 +202,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do
statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics") statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics")
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches") matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' }) matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) params: { state: 'not_started' })
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
params: { state: 'upcoming' })
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}") match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' }) invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}",
body: { state: 'in_progress' })
stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' }) invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' })
forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
@ -225,7 +229,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do
score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}") score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}")
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 }) score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 })
forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}",
body: { points: 3 })
bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) }) bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics") playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics")
@ -268,9 +273,9 @@ RSpec.describe 'Backend API surface HTTP E2E' do
expect(matches_all[:status]).to eq(200) expect(matches_all[:status]).to eq(200)
expect(matches_all[:json].count).to eq(tournament_match_count(tournament)) expect(matches_all[:json].count).to eq(tournament_match_count(tournament))
expect(matches_not_started[:status]).to eq(200) expect(matches_not_started[:status]).to eq(200)
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id)) expect(matches_not_started[:json].pluck(:id)).to include(match.fetch(:id))
expect(matches_upcoming[:status]).to eq(200) expect(matches_upcoming[:status]).to eq(200)
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id)) expect(matches_upcoming[:json].pluck(:id)).to include(match.fetch(:id))
expect(match_show[:status]).to eq(200) expect(match_show[:status]).to eq(200)
expect(invalid_match_update[:status]).to eq(422) expect(invalid_match_update[:status]).to eq(422)
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state') expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
@ -336,31 +341,38 @@ RSpec.describe 'Backend API surface HTTP E2E' do
.fetch(:groups) .fetch(:groups)
.find { |item| item.fetch(:id) == group.fetch(:id) } .find { |item| item.fetch(:id) == group.fetch(:id) }
swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) } swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) }
upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
params: { state: 'upcoming' })
finish_group_match_with_winner!(match_id: fourth_match.fetch(:id)) finish_group_match_with_winner!(match_id: fourth_match.fetch(:id))
upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
params: { state: 'upcoming' })
expect(upcoming_before[:status]).to eq(200) expect(upcoming_before[:status]).to eq(200)
expect(upcoming_match_for_group(upcoming_before, group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id)) expect(upcoming_match_for_group(upcoming_before,
group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id))
expect(swap_response[:status]).to eq(200) expect(swap_response[:status]).to eq(200)
expect(swap_response[:json].map { |match| match[:id] }).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)]) expect(swap_response[:json].pluck(:id)).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)])
expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id)) expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id))
expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position)) expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position))
expect(swapped_matches.find { |match| match.fetch(:id) == first_match.fetch(:id) }.fetch(:position)).to eq(fourth_match.fetch(:position)) expect(swapped_matches.find do |match|
match.fetch(:id) == first_match.fetch(:id)
end.fetch(:position)).to eq(fourth_match.fetch(:position))
expect(upcoming_after_swap[:status]).to eq(200) expect(upcoming_after_swap[:status]).to eq(200)
expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id)) expect(upcoming_match_for_group(upcoming_after_swap,
group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
expect(upcoming_after_finish[:status]).to eq(200) expect(upcoming_after_finish[:status]).to eq(200)
expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id)) expect(upcoming_match_for_group(upcoming_after_finish,
group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id))
end end
def login_client(email:, password:) def login_client(email:, password:)
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.login!(email: email, password: password) response = client.login!(email:, password:)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
expect(client.authenticated?).to eq(true) expect(client.authenticated?).to eq(true)
@ -368,15 +380,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do
end end
def secondary_client def secondary_client
alt_email = ENV['TURNIERE_E2E_ALT_EMAIL'] alt_email = ENV.fetch('TURNIERE_E2E_ALT_EMAIL', nil)
alt_password = ENV['TURNIERE_E2E_ALT_PASSWORD'] alt_password = ENV.fetch('TURNIERE_E2E_ALT_PASSWORD', nil)
if alt_email && alt_password if alt_email && alt_password
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.login!(email: alt_email, password: alt_password) response = client.login!(email: alt_email, password: alt_password)
return client if response[:status] == 200 && client.authenticated? return client if response[:status] == 200 && client.authenticated?
end end
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
unique = unique_suffix unique = unique_suffix
response = client.post('/users', body: { response = client.post('/users', body: {
email: "http-e2e-secondary-#{unique}@example.com", email: "http-e2e-secondary-#{unique}@example.com",
@ -394,11 +406,11 @@ RSpec.describe 'Backend API surface HTTP E2E' do
payload = { payload = {
name: "#{name_prefix} #{unique_suffix}", name: "#{name_prefix} #{unique_suffix}",
description: 'HTTP API surface E2E group-stage tournament', description: 'HTTP API surface E2E group-stage tournament',
public: public, public:,
group_stage: true, group_stage: true,
playoff_teams_amount: 8, playoff_teams_amount: 8,
teams: 4.times.flat_map do |group_index| teams: 4.times.flat_map do |group_index|
4.times.map do |seed_index| Array.new(4) do |seed_index|
{ {
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -409,22 +421,22 @@ RSpec.describe 'Backend API surface HTTP E2E' do
response = client.post('/tournaments', body: payload) response = client.post('/tournaments', body: payload)
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) fetch_tournament(client:, tournament_id: response.dig(:json, :id))
end end
def create_playoff_tournament(client:, public:, name_prefix:) def create_playoff_tournament(client:, public:, name_prefix:)
payload = { payload = {
name: "#{name_prefix} #{unique_suffix}", name: "#{name_prefix} #{unique_suffix}",
description: 'HTTP API surface E2E playoff tournament', description: 'HTTP API surface E2E playoff tournament',
public: public, public:,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
end end
} }
response = client.post('/tournaments', body: payload) response = client.post('/tournaments', body: payload)
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) fetch_tournament(client:, tournament_id: response.dig(:json, :id))
end end
def fetch_tournament(client:, tournament_id:) def fetch_tournament(client:, tournament_id:)

View File

@ -80,7 +80,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
name: "#{name_prefix} #{SecureRandom.hex(3)}", name: "#{name_prefix} #{SecureRandom.hex(3)}",
description: 'Websocket beamer tournament', description: 'Websocket beamer tournament',
public: true, public: true,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
end end
}) })
@ -89,13 +89,13 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
end end
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20) def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if payload.fetch('type') == event_type && return payload if payload.fetch('type') == event_type &&
payload.dig('beamer', 'display_state') == display_state payload.dig('beamer', 'display_state') == display_state
raise "beamer websocket payload not received within #{timeout}s" if Time.now >= deadline raise "beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end
end end

View File

@ -40,10 +40,12 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id)) follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url) cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id), beamer_name: follower_default_beamer.fetch(:name)) cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id),
beamer_name: follower_default_beamer.fetch(:name))
cable_client.wait_for_beamer_payload! cable_client.wait_for_beamer_payload!
source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id), beamer_name: DEFAULT_BEAMER_NAME) source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id),
beamer_name: DEFAULT_BEAMER_NAME)
update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: { update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: {
display_state: 'groups' display_state: 'groups'
}) })
@ -71,7 +73,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
name: "#{name_prefix} #{SecureRandom.hex(3)}", name: "#{name_prefix} #{SecureRandom.hex(3)}",
description: 'Follower beamer websocket source tournament', description: 'Follower beamer websocket source tournament',
public: true, public: true,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
end end
}) })
@ -107,25 +109,25 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
end end
def wait_for_default_beamer!(tournament_id:, timeout: 20) def wait_for_default_beamer!(tournament_id:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_name: DEFAULT_BEAMER_NAME) beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_name: DEFAULT_BEAMER_NAME)
return beamer if beamer.fetch(:name) == DEFAULT_BEAMER_NAME return beamer if beamer.fetch(:name) == DEFAULT_BEAMER_NAME
raise "follower default beamer not present within #{timeout}s" if Time.now >= deadline raise "follower default beamer not present within #{timeout}s" if Time.zone.now >= deadline
sleep 0.5 sleep 0.5
end end
end end
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20) def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if payload.fetch('type') == event_type && return payload if payload.fetch('type') == event_type &&
payload.dig('beamer', 'display_state') == display_state payload.dig('beamer', 'display_state') == display_state
raise "follower beamer websocket payload not received within #{timeout}s" if Time.now >= deadline raise "follower beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end
end end

View File

@ -23,7 +23,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
let(:sync_token) { "sync-#{SecureRandom.hex(8)}" } let(:sync_token) { "sync-#{SecureRandom.hex(8)}" }
let(:source_owner_client) { login_client(base_url: source_base_url, email: owner_email, password: owner_password) } let(:source_owner_client) { login_client(base_url: source_base_url, email: owner_email, password: owner_password) }
let(:follower_owner_client) { login_client(base_url: follower_base_url, email: owner_email, password: owner_password) } let(:follower_owner_client) do
login_client(base_url: follower_base_url, email: owner_email, password: owner_password)
end
let(:source_anonymous_client) { TurniereE2E::ApiClient.new(base_url: source_base_url) } let(:source_anonymous_client) { TurniereE2E::ApiClient.new(base_url: source_base_url) }
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) } let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
@ -43,23 +45,24 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
sync_auth_token: sync_token sync_auth_token: sync_token
}) })
expect(configure_sync[:status]).to eq(200) expect(configure_sync[:status]).to eq(200)
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) exercise_unrelated_requests!(source_noise:, follower_noise:)
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(tournament_signature(follower)).to eq(tournament_signature(source))
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
log_phase!('swap matches on leader and verify follower behavior') log_phase!('swap matches on leader and verify follower behavior')
source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
first_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first first_source_group = source_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) }
second_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }[1] second_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }[1]
first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) } first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }
first_position_match = first_group_matches.fetch(0) first_position_match = first_group_matches.fetch(0)
fourth_position_match = first_group_matches.fetch(3) fourth_position_match = first_group_matches.fetch(3)
wrong_group_match = second_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }.first wrong_group_match = second_source_group.fetch(:matches).min_by { |match| match.fetch(:position) }
follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
first_follower_group = follower_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first first_follower_group = follower_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) }
follower_first_position_match = first_follower_group.fetch(:matches).find do |match| follower_first_position_match = first_follower_group.fetch(:matches).find do |match|
match_team_names(match) == match_team_names(first_position_match) match_team_names(match) == match_team_names(first_position_match)
end end
@ -76,15 +79,18 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
fourth_position_match.fetch(:position) fourth_position_match.fetch(:position)
]) ])
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(tournament_signature(follower)).to eq(tournament_signature(source))
expect(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq( expect(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq(
group_match_positions(follower, group_number: first_follower_group.fetch(:number)) group_match_positions(follower, group_number: first_follower_group.fetch(:number))
) )
expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq( expect(group_match_positions(source,
group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq(
fourth_position_match.fetch(:position) fourth_position_match.fetch(:position)
) )
expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq( expect(group_match_positions(source,
group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq(
first_position_match.fetch(:position) first_position_match.fetch(:position)
) )
@ -97,7 +103,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
expect(wrong_group_swap[:status]).to eq(422) expect(wrong_group_swap[:status]).to eq(422)
expect(wrong_group_swap.dig(:json, :error)).to eq('Matches must be different group matches from the same group') expect(wrong_group_swap.dig(:json, :error)).to eq('Matches must be different group matches from the same group')
source_after_invalid_swap = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source_after_invalid_swap = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id)) follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client,
tournament_id: follower.fetch(:id))
expect(tournament_signature(source_after_invalid_swap)).to eq(source_before_invalid_swap) expect(tournament_signature(source_after_invalid_swap)).to eq(source_before_invalid_swap)
expect(tournament_signature(follower_after_invalid_swap)).to eq(follower_before_invalid_swap) expect(tournament_signature(follower_after_invalid_swap)).to eq(follower_before_invalid_swap)
@ -116,7 +123,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
intermediate_round_participants_amount: 0 intermediate_round_participants_amount: 0
}) })
expect(update_cutoff[:status]).to eq(200) expect(update_cutoff[:status]).to eq(200)
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) exercise_unrelated_requests!(source_noise:, follower_noise:)
source_group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source_group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } .fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
@ -127,20 +134,25 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
group_team_map(source_group_stage.fetch(:groups).last).fetch(2).fetch(:name) group_team_map(source_group_stage.fetch(:groups).last).fetch(2).fetch(:name)
] ]
) )
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise', range: 2..3)) source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise',
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', range: 3..4)) range: 2..3))
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise',
range: 3..4))
exercise_unrelated_requests!(source_noise:, follower_noise:)
log_phase!('play group stage with deciders') log_phase!('play group stage with deciders')
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }) play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by do |group|
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) group.fetch(:number)
end)
exercise_unrelated_requests!(source_noise:, follower_noise:)
log_phase!('verify playoff generation replicated') log_phase!('verify playoff generation replicated')
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(tournament_signature(follower)).to eq(tournament_signature(source))
expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1) expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1)
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
expect(source.fetch(:team_action_lists)).to eq([]) expect(source.fetch(:team_action_lists)).to eq([])
expect(follower.fetch(:team_action_lists)).to eq([]) expect(follower.fetch(:team_action_lists)).to eq([])
@ -149,7 +161,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
expect(created_list.fetch(:key)).to eq(playoff_token_list_key) expect(created_list.fetch(:key)).to eq(playoff_token_list_key)
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(playoff_token_list(source)).not_to be_nil expect(playoff_token_list(source)).not_to be_nil
expect(playoff_token_list(follower)).not_to be_nil expect(playoff_token_list(follower)).not_to be_nil
expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) }) expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
@ -173,44 +186,54 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it') log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it')
source_token_item = playoff_token_list(source).fetch(:team_action_items).first source_token_item = playoff_token_list(source).fetch(:team_action_items).first
source_token_update = source_owner_client.patch( source_token_update = source_owner_client.patch(
"/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(:team, :id)}", "/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(
:team, :id
)}",
body: { completed: true } body: { completed: true }
) )
expect(source_token_update[:status]).to eq(200) expect(source_token_update[:status]).to eq(200)
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(playoff_token_item_completed?(source, team_name: source_token_item.dig(:team, :name))).to eq(true) expect(playoff_token_item_completed?(source, team_name: source_token_item.dig(:team, :name))).to eq(true)
expect(playoff_token_item_completed?(follower, team_name: source_token_item.dig(:team, :name))).to eq(true) expect(playoff_token_item_completed?(follower, team_name: source_token_item.dig(:team, :name))).to eq(true)
follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name)) follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name))
locked_token_update = follower_owner_client.patch( locked_token_update = follower_owner_client.patch(
"/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(:team, :id)}", "/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(
:team, :id
)}",
body: { completed: false } body: { completed: false }
) )
expect(locked_token_update[:status]).to eq(423) expect(locked_token_update[:status]).to eq(423)
expect(locked_token_update.dig(:json, :error)).to eq('Tournament is in read only follower mode') expect(locked_token_update.dig(:json, :error)).to eq('Tournament is in read only follower mode')
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise', range: 2..4)) source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise',
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', range: 2..5)) range: 2..4))
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise',
range: 2..5))
exercise_unrelated_requests!(source_noise:, follower_noise:)
log_phase!('finish playoff bracket') log_phase!('finish playoff bracket')
finish_playoff_bracket!(source_id: source.fetch(:id)) finish_playoff_bracket!(source_id: source.fetch(:id))
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) exercise_unrelated_requests!(source_noise:, follower_noise:)
log_phase!('verify final follower state and takeover') log_phase!('verify final follower state and takeover')
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
follower_tournament_id: follower.fetch(:id))
expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(tournament_signature(follower)).to eq(tournament_signature(source))
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}", body: { read_only_mode: false }) disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}",
body: { read_only_mode: false })
expect(disable_follower[:status]).to eq(200) expect(disable_follower[:status]).to eq(200)
follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id)) follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id))
writable_team = follower_after_disable.fetch(:teams).first writable_team = follower_after_disable.fetch(:teams).first
takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}", body: { name: "#{writable_team.fetch(:name)} takeover" }) takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}",
body: { name: "#{writable_team.fetch(:name)} takeover" })
expect(takeover_update[:status]).to eq(200) expect(takeover_update[:status]).to eq(200)
end end
@ -220,8 +243,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
end end
def login_client(base_url:, email:, password:) def login_client(base_url:, email:, password:)
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.login!(email: email, password: password) response = client.login!(email:, password:)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
expect(client.authenticated?).to eq(true) expect(client.authenticated?).to eq(true)
@ -232,11 +255,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
payload = { payload = {
name: "#{name_prefix} #{unique_suffix}", name: "#{name_prefix} #{unique_suffix}",
description: 'Follower sync source tournament', description: 'Follower sync source tournament',
public: public, public:,
group_stage: true, group_stage: true,
playoff_teams_amount: 8, playoff_teams_amount: 8,
teams: 4.times.flat_map do |group_index| teams: 4.times.flat_map do |group_index|
4.times.map do |seed_index| Array.new(4) do |seed_index|
{ {
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -247,34 +270,34 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
response = client.post('/tournaments', body: payload) response = client.post('/tournaments', body: payload)
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) fetch_tournament(client:, tournament_id: response.dig(:json, :id))
end end
def create_playoff_tournament(client:, public:, name_prefix:) def create_playoff_tournament(client:, public:, name_prefix:)
payload = { payload = {
name: "#{name_prefix} #{unique_suffix}", name: "#{name_prefix} #{unique_suffix}",
description: 'Unrelated playoff tournament', description: 'Unrelated playoff tournament',
public: public, public:,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
end end
} }
response = client.post('/tournaments', body: payload) response = client.post('/tournaments', body: payload)
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) fetch_tournament(client:, tournament_id: response.dig(:json, :id))
end end
def create_follower_tournament(client:, public:, name_prefix:) def create_follower_tournament(client:, public:, name_prefix:)
response = client.post('/tournaments', body: { response = client.post('/tournaments', body: {
name: "#{name_prefix} #{unique_suffix}", name: "#{name_prefix} #{unique_suffix}",
description: 'Follower tournament', description: 'Follower tournament',
public: public, public:,
read_only_mode: true, read_only_mode: true,
sync_auth_token: sync_token sync_auth_token: sync_token
}) })
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) fetch_tournament(client:, tournament_id: response.dig(:json, :id))
end end
def fetch_tournament(client:, tournament_id:) def fetch_tournament(client:, tournament_id:)
@ -298,9 +321,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
amount = rand(range) amount = rand(range)
Array.new(amount) do |index| Array.new(amount) do |index|
if index.even? if index.even?
create_playoff_tournament(client: client, public: false, name_prefix: "#{prefix} Playoff #{index}") create_playoff_tournament(client:, public: false, name_prefix: "#{prefix} Playoff #{index}")
else else
create_group_stage_tournament(client: client, public: true, name_prefix: "#{prefix} Groups #{index}") create_group_stage_tournament(client:, public: true, name_prefix: "#{prefix} Groups #{index}")
end end
end end
end end
@ -316,7 +339,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name }) update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name })
expect(update[:status]).to eq(200) expect(update[:status]).to eq(200)
refreshed = fetch_tournament(client: client, tournament_id: tournament.fetch(:id)) refreshed = fetch_tournament(client:, tournament_id: tournament.fetch(:id))
team = refreshed.fetch(:teams).first team = refreshed.fetch(:teams).first
return if team.nil? return if team.nil?
@ -340,21 +363,21 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
end end
def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20) def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
last_progress_at = Time.now last_progress_at = Time.zone.now
loop do loop do
source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id) source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id)
follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id) follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id)
return follower if tournament_signature(source) == tournament_signature(follower) return follower if tournament_signature(source) == tournament_signature(follower)
if Time.now - last_progress_at >= 2 if Time.zone.now - last_progress_at >= 2
puts "sync wait source=#{sync_progress_signature(source).inspect} follower=#{sync_progress_signature(follower).inspect}" puts "sync wait source=#{sync_progress_signature(source).inspect} follower=#{sync_progress_signature(follower).inspect}"
$stdout.flush $stdout.flush
last_progress_at = Time.now last_progress_at = Time.zone.now
end end
if Time.now >= deadline if Time.zone.now >= deadline
raise <<~ERROR raise <<~ERROR
Follower did not catch up within #{timeout}s Follower did not catch up within #{timeout}s
source_signature=#{tournament_signature(source).inspect} source_signature=#{tournament_signature(source).inspect}
@ -374,8 +397,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
end end
def synced_team_id_pairs(source:, follower:) def synced_team_id_pairs(source:, follower:)
source_by_name = source.fetch(:teams).each_with_object({}) do |team, teams_by_name| source_by_name = source.fetch(:teams).index_by do |team|
teams_by_name[team.fetch(:name)] = team team.fetch(:name)
end end
follower.fetch(:teams).filter_map do |team| follower.fetch(:teams).filter_map do |team|
source_team = source_by_name[team.fetch(:name)] source_team = source_by_name[team.fetch(:name)]
@ -400,7 +423,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
expect(stage_finish[:status]).to eq(422) expect(stage_finish[:status]).to eq(422)
expect(stage_finish.dig(:json, :error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place') expect(stage_finish.dig(:json,
:error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place')
expect(stage_finish.dig(:json, :blocking_ties).size).to be >= 2 expect(stage_finish.dig(:json, :blocking_ties).size).to be >= 2
stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index| stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index|
@ -422,7 +446,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
expect(finish_decider[:status]).to eq(200) expect(finish_decider[:status]).to eq(200)
end end
finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}",
body: { state: 'finished' })
expect(finish_stage_after_decider[:status]).to eq(200) expect(finish_stage_after_decider[:status]).to eq(200)
end end
@ -466,7 +491,10 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
playoff_matches = tournament.fetch(:stages) playoff_matches = tournament.fetch(:stages)
.select { |stage| stage.fetch(:level) >= 0 } .select { |stage| stage.fetch(:level) >= 0 }
.flat_map { |stage| stage.fetch(:matches) } .flat_map { |stage| stage.fetch(:matches) }
.select { |match| %w[not_started in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2 } .select do |match|
%w[not_started
in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2
end
.sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] } .sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] }
break if playoff_matches.empty? break if playoff_matches.empty?
@ -488,7 +516,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
[first_score, second_score].each_with_index do |points, index| [first_score, second_score].each_with_index do |points, index|
response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}",
body: { points: })
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
end end
@ -504,7 +533,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
instant_finalists_amount: tournament.fetch(:instant_finalists_amount), instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount), intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
timestamp: tournament[:timestamp], timestamp: tournament[:timestamp],
teams: tournament.fetch(:teams).map { |team| team.slice(:name, :advancing_from_group_stage) }.sort_by { |team| team.fetch(:name) }, teams: tournament.fetch(:teams).map do |team|
team.slice(:name, :advancing_from_group_stage)
end.sort_by { |team| team.fetch(:name) },
team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list| team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list|
{ {
key: list.fetch(:key), key: list.fetch(:key),
@ -596,6 +627,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
end end
def playoff_token_item_completed?(tournament, team_name:) def playoff_token_item_completed?(tournament, team_name:)
find_team_action_item(tournament, team_name: team_name).fetch(:completed) find_team_action_item(tournament, team_name:).fetch(:completed)
end end
end end

View File

@ -27,7 +27,8 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) } let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
it 'broadcasts follower websocket updates after source sync import' do it 'broadcasts follower websocket updates after source sync import' do
source = create_group_stage_tournament(client: source_client, base_url: source_base_url, name_prefix: 'Ws Sync Source') source = create_group_stage_tournament(client: source_client, base_url: source_base_url,
name_prefix: 'Ws Sync Source')
follower = create_follower_tournament(name_prefix: 'Ws Sync Follower') follower = create_follower_tournament(name_prefix: 'Ws Sync Follower')
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: { configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
@ -61,7 +62,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
end end
def login_client(base_url:) def login_client(base_url:)
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.login!(email: owner_email, password: owner_password) response = client.login!(email: owner_email, password: owner_password)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
client client
@ -75,7 +76,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
group_stage: true, group_stage: true,
playoff_teams_amount: 4, playoff_teams_amount: 4,
teams: 2.times.flat_map do |group_index| teams: 2.times.flat_map do |group_index|
4.times.map do |seed_index| Array.new(4) do |seed_index|
{ {
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -84,7 +85,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
end end
}) })
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(base_url: base_url, tournament_id: response.dig(:json, :id)) fetch_tournament(base_url:, tournament_id: response.dig(:json, :id))
end end
def create_follower_tournament(name_prefix:) def create_follower_tournament(name_prefix:)
@ -101,7 +102,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
end end
def fetch_tournament(base_url:, tournament_id:) def fetch_tournament(base_url:, tournament_id:)
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.get("/tournaments/#{tournament_id}") response = client.get("/tournaments/#{tournament_id}")
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
response.fetch(:json) response.fetch(:json)
@ -141,7 +142,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
[first_score, second_score].each_with_index do |points, index| [first_score, second_score].each_with_index do |points, index|
score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
expect(score_update[:status]).to eq(200) expect(score_update[:status]).to eq(200)
end end
@ -150,24 +151,24 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
end end
def wait_for_list_presence!(tournament_id, timeout: 20) def wait_for_list_presence!(tournament_id, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
follower = fetch_tournament(base_url: follower_base_url, tournament_id: tournament_id) follower = fetch_tournament(base_url: follower_base_url, tournament_id:)
return follower if follower.fetch(:team_action_lists, []).any? return follower if follower.fetch(:team_action_lists, []).any?
raise "follower list not present within #{timeout}s" if Time.now >= deadline raise "follower list not present within #{timeout}s" if Time.zone.now >= deadline
sleep 0.5 sleep 0.5
end end
end end
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20) def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if team_action_item_completed?(payload, team_name: team_name) == completed return payload if team_action_item_completed?(payload, team_name:) == completed
raise "follower websocket payload not received within #{timeout}s" if Time.now >= deadline raise "follower websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end

View File

@ -14,10 +14,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') } let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') }
let(:runner) do let(:runner) do
TurniereE2E::ScenarioRunner.new( TurniereE2E::ScenarioRunner.new(
base_url: base_url, base_url:,
email: ENV.fetch('TURNIERE_E2E_EMAIL'), email: ENV.fetch('TURNIERE_E2E_EMAIL'),
password: ENV.fetch('TURNIERE_E2E_PASSWORD'), password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
username: ENV['TURNIERE_E2E_USERNAME'] username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
) )
end end
@ -31,7 +31,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16) expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16)
playoffs_created = result.dig(:checkpoints, :playoffs_created, :stage_summaries) playoffs_created = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
playoff_entry_stage = playoffs_created.select { |stage| stage[:level] != -1 }.max_by { |stage| stage[:level] } playoff_entry_stage = playoffs_created.reject { |stage| stage[:level] == -1 }.max_by { |stage| stage[:level] }
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)
@ -49,7 +49,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
expect(created[:intermediate_round_participants_amount]).to eq(2) expect(created[:intermediate_round_participants_amount]).to eq(2)
playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries) playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
.select { |stage| stage[:level] != -1 } .reject { |stage| stage[:level] == -1 }
.max_by { |stage| stage[:level] } .max_by { |stage| stage[:level] }
expect(playoff_entry_stage[:state]).to eq('intermediate_stage') expect(playoff_entry_stage[:state]).to eq('intermediate_stage')
expect(playoff_entry_stage[:match_states].count('single_team')).to eq(3) expect(playoff_entry_stage[:match_states].count('single_team')).to eq(3)
@ -76,7 +76,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
expect(positions.fetch('G1-S2')).to eq(expected_positions[1]) expect(positions.fetch('G1-S2')).to eq(expected_positions[1])
if direct_result == :drawn if direct_result == :drawn
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
:received_points))
.to eq( .to eq(
team_name: 'G1-S1', team_name: 'G1-S1',
position: 1, position: 1,
@ -85,7 +86,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
scored_points: 4, scored_points: 4,
received_points: 1 received_points: 1
) )
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
:received_points))
.to eq( .to eq(
team_name: 'G1-S2', team_name: 'G1-S2',
position: 1, position: 1,
@ -96,9 +98,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
) )
else else
expected_first_group_points = direct_result == :lost ? 9 : 6 expected_first_group_points = direct_result == :lost ? 9 : 6
expected_second_group_points = direct_result == :lost ? 6 : 6 expected_second_group_points = 6
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
:received_points))
.to eq( .to eq(
team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2', team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2',
position: 1, position: 1,
@ -107,7 +110,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
scored_points: 5, scored_points: 5,
received_points: 2 received_points: 2
) )
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
:received_points))
.to eq( .to eq(
team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1', team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1',
position: 2, position: 2,
@ -122,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
[2, 3, 5, 8].each do |team_count| [2, 3, 5, 8].each do |team_count|
it "drives a #{team_count}-team playoff-only tournament over HTTP to completion" do it "drives a #{team_count}-team playoff-only tournament over HTTP to completion" do
result = runner.run_playoff_only(team_count: team_count) result = runner.run_playoff_only(team_count:)
completed_stages = result.dig(:checkpoints, :completed, :stage_summaries) completed_stages = result.dig(:checkpoints, :completed, :stage_summaries)
final_stage = completed_stages.find { |stage| stage[:level] == 0 } final_stage = completed_stages.find { |stage| stage[:level] == 0 }

View File

@ -15,17 +15,20 @@ RSpec.describe 'Tournament rendering HTTP E2E' do
let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' } let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' }
let(:runner) do let(:runner) do
TurniereE2E::ScenarioRunner.new( TurniereE2E::ScenarioRunner.new(
base_url: base_url, base_url:,
email: ENV.fetch('TURNIERE_E2E_EMAIL'), email: ENV.fetch('TURNIERE_E2E_EMAIL'),
password: ENV.fetch('TURNIERE_E2E_PASSWORD'), password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
username: ENV['TURNIERE_E2E_USERNAME'] username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
) )
end end
[ [
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, max_server_duration_ms: 1000 }, { group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000,
{ group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000, max_server_duration_ms: 5000 }, max_server_duration_ms: 1000 },
{ group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000, max_server_duration_ms: 5000 } { group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000,
max_server_duration_ms: 5000 },
{ group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000,
max_server_duration_ms: 5000 }
].each do |scenario| ].each do |scenario|
it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do
result = runner.run_group_stage_render_profile( result = runner.run_group_stage_render_profile(

View File

@ -21,7 +21,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" } let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" }
let(:client) { login_client } let(:client) { login_client }
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
it 'roundtrips current snapshot into follower tournament through test_sync endpoint' do it 'roundtrips current snapshot into follower tournament through test_sync endpoint' do
follower = create_follower_tournament(name_prefix: 'Sync Test Follower') follower = create_follower_tournament(name_prefix: 'Sync Test Follower')
@ -74,7 +74,8 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
expect(update_by_item_id[:status]).to eq(200) expect(update_by_item_id[:status]).to eq(200)
update_by_business_key = client.patch( update_by_business_key = client.patch(
"/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}", "/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team,
:id)}",
body: { completed: true } body: { completed: true }
) )
expect(update_by_business_key[:status]).to eq(200) expect(update_by_business_key[:status]).to eq(200)
@ -118,7 +119,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
end end
def login_client def login_client
api_client = TurniereE2E::ApiClient.new(base_url: base_url) api_client = TurniereE2E::ApiClient.new(base_url:)
response = api_client.login!(email: owner_email, password: owner_password) response = api_client.login!(email: owner_email, password: owner_password)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
@ -133,7 +134,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
group_stage: true, group_stage: true,
playoff_teams_amount: 4, playoff_teams_amount: 4,
teams: 2.times.flat_map do |group_index| teams: 2.times.flat_map do |group_index|
4.times.map do |seed_index| Array.new(4) do |seed_index|
{ {
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -141,10 +142,10 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
end end
end end
} }
body[:sync_target_url] = sync_target_url unless sync_target_url.nil? || sync_target_url.empty? body[:sync_target_url] = sync_target_url if sync_target_url.present?
body[:sync_auth_token] = sync_auth_token unless sync_auth_token.nil? || sync_auth_token.empty? body[:sync_auth_token] = sync_auth_token if sync_auth_token.present?
response = client.post('/tournaments', body: body) response = client.post('/tournaments', body:)
expect(response[:status]).to eq(201) expect(response[:status]).to eq(201)
fetch_tournament(response.dig(:json, :id)) fetch_tournament(response.dig(:json, :id))
end end
@ -201,7 +202,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
[first_score, second_score].each_with_index do |points, index| [first_score, second_score].each_with_index do |points, index|
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
expect(score_update[:status]).to eq(200) expect(score_update[:status]).to eq(200)
end end

View File

@ -21,7 +21,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') } let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
let(:client) { login_client } let(:client) { login_client }
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
it 'broadcasts initial and updated team action list state over websocket' do it 'broadcasts initial and updated team action list state over websocket' do
tournament = create_group_stage_tournament(name_prefix: 'Ws Leader') tournament = create_group_stage_tournament(name_prefix: 'Ws Leader')
@ -30,7 +30,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
tournament = fetch_tournament(tournament.fetch(:id)) tournament = fetch_tournament(tournament.fetch(:id))
token_list = find_playoff_token_list!(tournament) token_list = find_playoff_token_list!(tournament)
cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url) cable_client = TurniereE2E::ActionCableClient.new(base_url:)
cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id)) cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id))
initial_payload = cable_client.wait_for_payload! initial_payload = cable_client.wait_for_payload!
@ -50,7 +50,8 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true) expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true)
update_by_business_key = client.patch( update_by_business_key = client.patch(
"/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}", "/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team,
:id)}",
body: { completed: true } body: { completed: true }
) )
expect(update_by_business_key[:status]).to eq(200) expect(update_by_business_key[:status]).to eq(200)
@ -67,7 +68,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
end end
def login_client def login_client
api_client = TurniereE2E::ApiClient.new(base_url: base_url) api_client = TurniereE2E::ApiClient.new(base_url:)
response = api_client.login!(email: owner_email, password: owner_password) response = api_client.login!(email: owner_email, password: owner_password)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
@ -82,7 +83,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
group_stage: true, group_stage: true,
playoff_teams_amount: 4, playoff_teams_amount: 4,
teams: 2.times.flat_map do |group_index| teams: 2.times.flat_map do |group_index|
4.times.map do |seed_index| Array.new(4) do |seed_index|
{ {
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -134,7 +135,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
[first_score, second_score].each_with_index do |points, index| [first_score, second_score].each_with_index do |points, index|
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
expect(score_update[:status]).to eq(200) expect(score_update[:status]).to eq(200)
end end
@ -143,12 +144,12 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
end end
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20) def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if team_action_item_completed?(payload, team_name: team_name) == completed return payload if team_action_item_completed?(payload, team_name:) == completed
raise "team action websocket payload not received within #{timeout}s" if Time.now >= deadline raise "team action websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end

View File

@ -63,7 +63,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
end end
def login_client(base_url:) def login_client(base_url:)
client = TurniereE2E::ApiClient.new(base_url: base_url) client = TurniereE2E::ApiClient.new(base_url:)
response = client.login!(email: owner_email, password: owner_password) response = client.login!(email: owner_email, password: owner_password)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
client client
@ -74,7 +74,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
name: "#{name_prefix} #{SecureRandom.hex(3)}", name: "#{name_prefix} #{SecureRandom.hex(3)}",
description: 'Follower websocket timer source tournament', description: 'Follower websocket timer source tournament',
public: false, public: false,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
end end
}) })
@ -96,12 +96,12 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
end end
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20) def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if payload.fetch('timer_mode') == timer_mode return payload if payload.fetch('timer_mode') == timer_mode
raise "follower timer websocket payload not received within #{timeout}s" if Time.now >= deadline raise "follower timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end
end end

View File

@ -25,7 +25,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
it 'broadcasts timer state changes over websocket' do it 'broadcasts timer state changes over websocket' do
tournament = create_playoff_tournament(name_prefix: 'Ws Timer') tournament = create_playoff_tournament(name_prefix: 'Ws Timer')
cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url) cable_client = TurniereE2E::ActionCableClient.new(base_url:)
cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id)) cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id))
initial_payload = cable_client.wait_for_timer_payload! initial_payload = cable_client.wait_for_timer_payload!
@ -54,7 +54,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
end end
def login_client def login_client
api_client = TurniereE2E::ApiClient.new(base_url: base_url) api_client = TurniereE2E::ApiClient.new(base_url:)
response = api_client.login!(email: owner_email, password: owner_password) response = api_client.login!(email: owner_email, password: owner_password)
expect(response[:status]).to eq(200) expect(response[:status]).to eq(200)
@ -66,7 +66,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
name: "#{name_prefix} #{SecureRandom.hex(3)}", name: "#{name_prefix} #{SecureRandom.hex(3)}",
description: 'Websocket timer tournament', description: 'Websocket timer tournament',
public: false, public: false,
teams: 4.times.map do |index| teams: Array.new(4) do |index|
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" } { name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
end end
}) })
@ -75,12 +75,12 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
end end
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20) def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
deadline = Time.now + timeout deadline = Time.zone.now + timeout
loop do loop do
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max) payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
return payload if payload.fetch('timer_mode') == timer_mode return payload if payload.fetch('timer_mode') == timer_mode
raise "timer websocket payload not received within #{timeout}s" if Time.now >= deadline raise "timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
end end
end end
end end

View File

@ -10,7 +10,7 @@ FactoryBot.define do
name { Beamer::DEFAULT_NAME } name { Beamer::DEFAULT_NAME }
initialize_with do initialize_with do
tournament.beamers.find_or_initialize_by(name: name) tournament.beamers.find_or_initialize_by(name:)
end end
end end
end end

View File

@ -11,9 +11,9 @@ FactoryBot.define do
stage stage
after(:create) do |group, evaluator| after(:create) do |group, evaluator|
create_list(evaluator.match_factory, evaluator.match_count, group: group) create_list(evaluator.match_factory, evaluator.match_count, group:)
group.group_scores = group.teams.map do |team| group.group_scores = group.teams.map do |team|
create(:group_score, team: team, group: group) create(:group_score, team:, group:)
end end
end end
end end

View File

@ -13,17 +13,14 @@ FactoryBot.define do
intermediate_round_participants_amount { 0 } intermediate_round_participants_amount { 0 }
end end
after(:create) do |tournament, evaluator| after(:create) do |tournament, evaluator|
if evaluator.teams.present? tournament.teams = (evaluator.teams.presence || create_list(:team, evaluator.teams_count, tournament:))
tournament.teams = evaluator.teams
else
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
end
tournament.playoff_teams_amount = evaluator.playoff_teams_amount tournament.playoff_teams_amount = evaluator.playoff_teams_amount
tournament.instant_finalists_amount = evaluator.instant_finalists_amount tournament.instant_finalists_amount = evaluator.instant_finalists_amount
tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount
if tournament.playoff_teams_amount != tournament.instant_finalists_amount + tournament.intermediate_round_participants_amount / 2 if tournament.playoff_teams_amount != tournament.instant_finalists_amount + (tournament.intermediate_round_participants_amount / 2)
raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2' raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2'
end end
tournament.save! tournament.save!
end end
@ -36,7 +33,7 @@ FactoryBot.define do
(1..evaluator.stage_count).each do |level| (1..evaluator.stage_count).each do |level|
tournament.stages << create( tournament.stages << create(
:playoff_stage, :playoff_stage,
level: level, level:,
match_count: -1, match_count: -1,
match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match
) )

View File

@ -13,7 +13,7 @@ RSpec.describe AddGroupStageToTournament, type: :interactor do
@empty_tournament = create(:stageless_tournament) @empty_tournament = create(:stageless_tournament)
@group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0) @group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
@group_stage = create(:group_stage) @group_stage = create(:group_stage)
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values @groups = { 1 => create_list(:team, 4), 2 => create_list(:team, 4) }.values
@tournament_service_defaults = [78_345, 2_387] @tournament_service_defaults = [78_345, 2_387]
end end

View File

@ -14,7 +14,7 @@ RSpec.describe Beamer, type: :model do
end end
it 'allows same name in different tournaments' do it 'allows same name in different tournaments' do
beamer = create(:beamer, name: 'projector') create(:beamer, name: 'projector')
other = build(:beamer, name: 'projector', tournament: create(:tournament)) other = build(:beamer, name: 'projector', tournament: create(:tournament))
expect(other).to be_valid expect(other).to be_valid
end end

View File

@ -15,21 +15,21 @@ RSpec.describe GroupScore, type: :model do
let(:team_c) { create(:team) } let(:team_c) { create(:team) }
let(:group_score_a) do let(:group_score_a) do
create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4) create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
end end
let(:group_score_b) do let(:group_score_b) do
create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 7, received_points: 4) create(:group_score, group:, team: team_b, group_points: 6, scored_points: 7, received_points: 4)
end end
it 'prefers the winner of the direct comparison when the first three criteria are tied' do it 'prefers the winner of the direct comparison when the first three criteria are tied' do
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 3) create(:match_score, match:, team: team_a, points: 3)
create(:match_score, match: match, team: team_c, points: 1) create(:match_score, match:, team: team_c, points: 1)
end end
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 4) create(:match_score, match:, team: team_a, points: 4)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
group_score_a.reload group_score_a.reload
@ -40,9 +40,9 @@ RSpec.describe GroupScore, type: :model do
end end
it 'keeps the teams tied when their direct comparison was a draw' do it 'keeps the teams tied when their direct comparison was a draw' do
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
group_score_a.reload group_score_a.reload
@ -59,10 +59,10 @@ RSpec.describe GroupScore, type: :model do
let(:team_b) { create(:team) } let(:team_b) { create(:team) }
let(:group_score_a) do let(:group_score_a) do
create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4) create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
end end
let(:group_score_b) do let(:group_score_b) do
create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 7, received_points: 4) create(:group_score, group:, team: team_b, group_points: 3, scored_points: 7, received_points: 4)
end end
it 'reports the first deciding metric when points are different' do it 'reports the first deciding metric when points are different' do
@ -78,7 +78,7 @@ RSpec.describe GroupScore, type: :model do
it 'reports the direct comparison result when head-to-head resolves the tie' do it 'reports the direct comparison result when head-to-head resolves the tie' do
group_score_b.update!(group_points: 6) group_score_b.update!(group_points: 6)
match = create(:group_match, group: group, state: :finished).tap do |created_match| match = create(:group_match, group:, state: :finished).tap do |created_match|
create(:match_score, match: created_match, team: team_a, points: 3) create(:match_score, match: created_match, team: team_a, points: 3)
create(:match_score, match: created_match, team: team_b, points: 1) create(:match_score, match: created_match, team: team_b, points: 1)
end end
@ -95,7 +95,7 @@ RSpec.describe GroupScore, type: :model do
it 'reports an unresolved tie when head-to-head was a draw' do it 'reports an unresolved tie when head-to-head was a draw' do
group_score_b.update!(group_points: 6) group_score_b.update!(group_points: 6)
match = create(:group_match, group: group, state: :finished).tap do |created_match| match = create(:group_match, group:, state: :finished).tap do |created_match|
create(:match_score, match: created_match, team: team_a, points: 2) create(:match_score, match: created_match, team: team_a, points: 2)
create(:match_score, match: created_match, team: team_b, points: 2) create(:match_score, match: created_match, team: team_b, points: 2)
end end
@ -112,11 +112,11 @@ RSpec.describe GroupScore, type: :model do
it 'falls back to hidden points when direct comparison is still tied' do it 'falls back to hidden points when direct comparison is still tied' do
group_score_b.update!(group_points: 6) group_score_b.update!(group_points: 6)
direct_match = create(:group_match, group: group, state: :finished).tap do |created_match| direct_match = create(:group_match, group:, state: :finished).tap do |created_match|
create(:match_score, match: created_match, team: team_a, points: 2) create(:match_score, match: created_match, team: team_a, points: 2)
create(:match_score, match: created_match, team: team_b, points: 2) create(:match_score, match: created_match, team: team_b, points: 2)
end end
decider_match = create(:group_match, group: group, state: :finished, decider: true).tap do |created_match| decider_match = create(:group_match, group:, state: :finished, decider: true).tap do |created_match|
create(:match_score, match: created_match, team: team_a, hidden_points: 3) create(:match_score, match: created_match, team: team_a, hidden_points: 3)
create(:match_score, match: created_match, team: team_b, hidden_points: 1) create(:match_score, match: created_match, team: team_b, hidden_points: 1)
end end

View File

@ -36,7 +36,7 @@ rescue ActiveRecord::PendingMigrationError => e
end end
RSpec.configure do |config| RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures" config.fixture_path = Rails.root.join('spec/fixtures').to_s
# Run only focused tests # Run only focused tests
# TODO REVERT ME # TODO REVERT ME

View File

@ -4,7 +4,7 @@ RSpec.describe GroupStageService do
before do before do
@teams1 = create_list(:team, 4) @teams1 = create_list(:team, 4)
@teams2 = create_list(:team, 4) @teams2 = create_list(:team, 4)
@prepared_groups = Hash[1 => @teams1, 2 => @teams2].values @prepared_groups = { 1 => @teams1, 2 => @teams2 }.values
end end
describe '#generate_group_stage' do describe '#generate_group_stage' do
let(:prepared_groups_groupstage) do let(:prepared_groups_groupstage) do
@ -52,7 +52,7 @@ RSpec.describe GroupStageService do
end end
end end
# TODO test get_advancing_teams when test data for running group stage is ready # TODO: test get_advancing_teams when test data for running group stage is ready
describe '#get_group_object_from' do describe '#get_group_object_from' do
it 'returns a group' do it 'returns a group' do
@ -205,13 +205,13 @@ RSpec.describe GroupStageService do
team_a = create(:team) team_a = create(:team)
team_b = create(:team) team_b = create(:team)
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 4) create(:match_score, match:, team: team_a, points: 4)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2) create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2) create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
expect(GroupStageService.teams_sorted_by_group_scores(group.reload)).to eq([team_a, team_b]) expect(GroupStageService.teams_sorted_by_group_scores(group.reload)).to eq([team_a, team_b])
end end
@ -224,14 +224,14 @@ RSpec.describe GroupStageService do
playoff_teams_amount: 2, playoff_teams_amount: 2,
instant_finalists_amount: 2, instant_finalists_amount: 2,
intermediate_round_participants_amount: 0, intermediate_round_participants_amount: 0,
group_stage: group_stage) group_stage:)
end end
let(:group_stage) { create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0) } let(:group_stage) { create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0) }
let(:group) { create(:group, stage: group_stage, match_count: 0, number: 1) } let(:group) { create(:group, stage: group_stage, match_count: 0, number: 1) }
let(:team_a) { create(:team, tournament: tournament, name: 'A') } let(:team_a) { create(:team, tournament:, name: 'A') }
let(:team_b) { create(:team, tournament: tournament, name: 'B') } let(:team_b) { create(:team, tournament:, name: 'B') }
let(:team_c) { create(:team, tournament: tournament, name: 'C') } let(:team_c) { create(:team, tournament:, name: 'C') }
let(:team_d) { create(:team, tournament: tournament, name: 'D') } let(:team_d) { create(:team, tournament:, name: 'D') }
before do before do
group_stage.groups = [group] group_stage.groups = [group]
@ -240,13 +240,13 @@ RSpec.describe GroupStageService do
end end
it 'returns direct-comparison metadata for teams separated by head-to-head' do it 'returns direct-comparison metadata for teams separated by head-to-head' do
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 4) create(:match_score, match:, team: team_a, points: 4)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2) score_a = create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
score_b = create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2) score_b = create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
@ -269,17 +269,18 @@ RSpec.describe GroupStageService do
end end
it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
@ -301,17 +302,18 @@ RSpec.describe GroupStageService do
end end
it 'requests a decider when tied teams both advance but their seeding stays unresolved' do it 'requests a decider when tied teams both advance but their seeding stays unresolved' do
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
@ -333,17 +335,18 @@ RSpec.describe GroupStageService do
end end
it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :in_progress).tap do |match| create(:group_match, group:, state: :in_progress).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 1, scored_points: 2, received_points: 2) score_a = create(:group_score, group:, team: team_a, group_points: 1, scored_points: 2, received_points: 2)
score_b = create(:group_score, group: group, team: team_b, group_points: 1, scored_points: 2, received_points: 2) score_b = create(:group_score, group:, team: team_b, group_points: 1, scored_points: 2, received_points: 2)
score_c = create(:group_score, group: group, team: team_c, group_points: 0, scored_points: 0, received_points: 0) score_c = create(:group_score, group:, team: team_c, group_points: 0, scored_points: 0, received_points: 0)
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 0, received_points: 0) score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 0, received_points: 0)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
@ -352,12 +355,15 @@ RSpec.describe GroupStageService do
end end
it 'does not expose unresolved tie metadata outside the advancing cutoff before the group is finished' do it 'does not expose unresolved tie metadata outside the advancing cutoff before the group is finished' do
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
intermediate_round_participants_amount: 0)
score_a = create(:group_score, group: group, team: team_a, group_points: 9, scored_points: 5, received_points: 1, position: 1) score_a = create(:group_score, group:, team: team_a, group_points: 9, scored_points: 5, received_points: 1,
score_b = create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 4, received_points: 2, position: 2) position: 1)
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) score_b = create(:group_score, group:, team: team_b, group_points: 6, scored_points: 4, received_points: 2,
score_d = create(:group_score, group: group, team: team_d, group_points: 3, scored_points: 2, received_points: 3) position: 2)
score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
score_d = create(:group_score, group:, team: team_d, group_points: 3, scored_points: 2, received_points: 3)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
@ -367,21 +373,22 @@ RSpec.describe GroupStageService do
end end
it 'uses the decider hidden points to resolve a cutoff tie' do it 'uses the decider hidden points to resolve a cutoff tie' do
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :finished).tap do |match| create(:group_match, group:, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2) create(:match_score, match:, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2) create(:match_score, match:, team: team_b, points: 2)
end end
create(:group_match, group: group, state: :finished, decider: true).tap do |match| create(:group_match, group:, state: :finished, decider: true).tap do |match|
create(:match_score, match: match, team: team_a, hidden_points: 5) create(:match_score, match:, team: team_a, hidden_points: 5)
create(:match_score, match: match, team: team_b, hidden_points: 3) create(:match_score, match:, team: team_b, hidden_points: 3)
end end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
@ -417,7 +424,7 @@ RSpec.describe GroupStageService do
@tournament = create(:prepared_group_stage_tournament, @tournament = create(:prepared_group_stage_tournament,
group_stage: @group_stage, group_stage: @group_stage,
teams: teams, teams:,
playoff_teams_amount: 16, playoff_teams_amount: 16,
instant_finalists_amount: 16, instant_finalists_amount: 16,
intermediate_round_participants_amount: 0) intermediate_round_participants_amount: 0)
@ -427,7 +434,7 @@ RSpec.describe GroupStageService do
match.match_scores.each do |ms| match.match_scores.each do |ms|
# give the team 10 points minus the number in their name # give the team 10 points minus the number in their name
# this results in the team 0 always winning and getting to place 1 in the group etc. # this results in the team 0 always winning and getting to place 1 in the group etc.
ms.points = 10 - ms.team.name.split(' ').last.to_i ms.points = 10 - ms.team.name.split.last.to_i
ms.save! ms.save!
end end
match.state = 'finished' match.state = 'finished'
@ -450,7 +457,7 @@ RSpec.describe GroupStageService do
advancing_teams.each_with_index do |team, i| advancing_teams.each_with_index do |team, i|
# if index is even, the team should be of a first place; end in a 0 # if index is even, the team should be of a first place; end in a 0
# if index is odd, the team should be of a second place; end in a 1 # if index is odd, the team should be of a second place; end in a 1
team_quality = team.name.split(' ').last.to_i team_quality = team.name.split.last.to_i
expect(team_quality % 2).to be(i % 2) expect(team_quality % 2).to be(i % 2)
end end
end end
@ -469,9 +476,9 @@ RSpec.describe GroupStageService do
advancing_teams = GroupStageService.get_advancing_teams(@group_stage) advancing_teams = GroupStageService.get_advancing_teams(@group_stage)
advancing_teams.each_slice(2).to_a.each do |matchup| advancing_teams.each_slice(2).to_a.each do |matchup|
# this is the team that landed a first place in the group # this is the team that landed a first place in the group
first_place_team = matchup[0].name.split(' ')[1].to_i first_place_team = matchup[0].name.split[1].to_i
# this is the team that landed a second place in the group # this is the team that landed a second place in the group
second_place_team = matchup[1].name.split(' ')[1].to_i second_place_team = matchup[1].name.split[1].to_i
expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team) expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team)
end end
end end

View File

@ -62,7 +62,7 @@ RSpec.describe MatchService do
first_team = match.match_scores.first.team.name first_team = match.match_scores.first.team.name
second_team = match.match_scores.second.team.name second_team = match.match_scores.second.team.name
expect(first_team).to eq(teams[2 * index].name) expect(first_team).to eq(teams[2 * index].name)
expect(second_team).to eq(teams[2 * index + 1].name) expect(second_team).to eq(teams[(2 * index) + 1].name)
end end
end end
end end

View File

@ -61,7 +61,7 @@ RSpec.describe PlayoffStageService do
{ team_size: 9, expected_amount_of_playoff_stages: 4 }, { team_size: 9, expected_amount_of_playoff_stages: 4 },
{ team_size: 10, expected_amount_of_playoff_stages: 4 }, { team_size: 10, expected_amount_of_playoff_stages: 4 },
{ team_size: 16, expected_amount_of_playoff_stages: 4 }, { team_size: 16, expected_amount_of_playoff_stages: 4 },
{ team_size: 24, expected_amount_of_playoff_stages: 5 }, { team_size: 24, expected_amount_of_playoff_stages: 5 }
].each do |parameters| ].each do |parameters|
it "generates playoff stages for #{parameters[:team_size]} teams" do it "generates playoff stages for #{parameters[:team_size]} teams" do
amount_of_teams = parameters[:team_size] amount_of_teams = parameters[:team_size]

View File

@ -48,7 +48,8 @@ RSpec.describe TeamActionListService do
action_name: 'Break', action_name: 'Break',
source: 'unknown_source' source: 'unknown_source'
) )
end.to raise_error(TeamActionListService::InvalidSourceError, 'Unsupported team action list source: unknown_source') end.to raise_error(TeamActionListService::InvalidSourceError,
'Unsupported team action list source: unknown_source')
end end
it 'raises when source resolves to no teams' do it 'raises when source resolves to no teams' do
@ -62,7 +63,8 @@ RSpec.describe TeamActionListService do
action_name: 'Nothing', action_name: 'Nothing',
source: 'all_tournament_teams' source: 'all_tournament_teams'
) )
end.to raise_error(TeamActionListService::EmptySourceError, 'Source all_tournament_teams does not currently contain any teams') end.to raise_error(TeamActionListService::EmptySourceError,
'Source all_tournament_teams does not currently contain any teams')
end end
end end
end end

View File

@ -31,7 +31,7 @@ RSpec.describe 'Tournament live payloads' do
expect(serialized_match[:position]).to eq(match.position) expect(serialized_match[:position]).to eq(match.position)
expect(serialized_match.dig(:group, :id)).to eq(match.group_id) expect(serialized_match.dig(:group, :id)).to eq(match.group_id)
expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id) expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id)
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id)) expect(serialized_match[:teams].pluck(:id)).to match_array(match.teams.map(&:id))
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id)) expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
end end
end end

View File

@ -13,7 +13,7 @@ RSpec.describe TournamentSnapshotImporter do
snapshot = TournamentSnapshotBuilder.build(source) snapshot = TournamentSnapshotBuilder.build(source)
described_class.import!(tournament: follower, snapshot: snapshot) described_class.import!(tournament: follower, snapshot:)
follower.reload follower.reload
expect(follower.sync_source_tournament_id).to eq(source.id) expect(follower.sync_source_tournament_id).to eq(source.id)
@ -25,8 +25,12 @@ RSpec.describe TournamentSnapshotImporter do
expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count) expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count)
expect(follower.stages.count).to eq(source.stages.count) expect(follower.stages.count).to eq(source.stages.count)
expect(follower.matches.count).to eq(source.matches.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) }) expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map do |team|
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) }) team.fetch(:id)
end)
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map do |beamer|
beamer.fetch(:id)
end)
end end
it 'ignores stale snapshots that arrive after a newer one' do it 'ignores stale snapshots that arrive after a newer one' do
@ -54,7 +58,7 @@ RSpec.describe TournamentSnapshotImporter do
snapshot = TournamentSnapshotBuilder.build(source) snapshot = TournamentSnapshotBuilder.build(source)
described_class.import!(tournament: follower, snapshot: snapshot) described_class.import!(tournament: follower, snapshot:)
follower.reload follower.reload
expect(follower.id).not_to eq(source.id) expect(follower.id).not_to eq(source.id)
@ -62,7 +66,7 @@ RSpec.describe TournamentSnapshotImporter do
expect(follower.teams.count).to eq(128) expect(follower.teams.count).to eq(128)
expect(follower.stages.where(level: -1).first.groups.count).to eq(32) 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(64) expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(64)
expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count) expect(follower.matches.count(&:decider?)).to eq(source.matches.count(&:decider?))
expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id)) 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)) expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
end end
@ -95,15 +99,15 @@ RSpec.describe TournamentSnapshotImporter do
logo_url: '/logos/stream-b.png' logo_url: '/logos/stream-b.png'
) )
group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished) group_stage = Stage.create!(tournament:, level: -1, state: :finished)
advancing_teams = [] advancing_teams = []
32.times do |group_index| 32.times do |group_index|
group = Group.create!(stage: group_stage, number: group_index + 1) group = Group.create!(stage: group_stage, number: group_index + 1)
teams = 4.times.map do |seed_index| teams = Array.new(4) do |seed_index|
create( create(
:team, :team,
tournament: tournament, tournament:,
name: "Leader-G#{group_index + 1}-S#{seed_index + 1}" name: "Leader-G#{group_index + 1}-S#{seed_index + 1}"
) )
end end
@ -111,17 +115,15 @@ RSpec.describe TournamentSnapshotImporter do
teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero? teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero?
teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15 teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15
create_group_round_robin!(group: group, teams: teams) create_group_round_robin!(group:, teams:)
create_group_scores!(group: group, teams: teams) create_group_scores!(group:, teams:)
if group_index < 3 create_decider_match!(group:, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) 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)) advancing_teams.concat(teams.take(2))
end end
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams) result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: advancing_teams)
expect(result).to be_a_success expect(result).to be_a_success
tournament.reload tournament.reload
@ -140,23 +142,23 @@ RSpec.describe TournamentSnapshotImporter do
] ]
pairings.each_with_index do |(left_index, right_index, left_points, right_points), position| 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) match = Match.create!(group:, state: :finished, position:, decider: false)
MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0) MatchScore.create!(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) MatchScore.create!(match:, team: teams[right_index], points: right_points, hidden_points: 0)
end end
end end
def create_decider_match!(group:, teams:, hidden_points:) def create_decider_match!(group:, teams:, hidden_points:)
match = Match.create!(group: group, state: :finished, position: 6, decider: true) match = Match.create!(group:, state: :finished, position: 6, decider: true)
MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first) MatchScore.create!(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) MatchScore.create!(match:, team: teams.second, points: 0, hidden_points: hidden_points.last)
end end
def create_group_scores!(group:, teams:) def create_group_scores!(group:, teams:)
teams.each_with_index do |team, index| teams.each_with_index do |team, index|
GroupScore.create!( GroupScore.create!(
group: group, group:,
team: team, team:,
group_points: 9 - index, group_points: 9 - index,
scored_points: 12 - index, scored_points: 12 - index,
received_points: index, received_points: index,
@ -185,7 +187,7 @@ RSpec.describe TournamentSnapshotImporter do
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount, intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
timestamp: tournament.timestamp&.iso8601, timestamp: tournament.timestamp&.iso8601,
timer_mode: tournament.timer_mode, timer_mode: tournament.timer_mode,
teams: tournament.teams.order(:name).map { |team| team.name }, teams: tournament.teams.order(:name).map(&:name),
team_action_lists: tournament.team_action_lists.order(:key).map do |list| team_action_lists: tournament.team_action_lists.order(:key).map do |list|
{ {
key: list.key, key: list.key,

View File

@ -9,7 +9,7 @@ RSpec.describe TournamentSyncEnqueue do
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state', sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
sync_auth_token: 'shared-secret') sync_auth_token: 'shared-secret')
entry = TournamentSyncQueueEntry.create!( entry = TournamentSyncQueueEntry.create!(
tournament: tournament, tournament:,
snapshot_json: '{"old":true}', snapshot_json: '{"old":true}',
status: 'pending', status: 'pending',
attempts: 4, attempts: 4,

View File

@ -9,9 +9,10 @@ RSpec.describe TournamentSyncProcessor do
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state', sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
sync_auth_token: 'shared-secret') sync_auth_token: 'shared-secret')
TournamentSyncEnqueue.call(tournament) TournamentSyncEnqueue.call(tournament)
entry = TournamentSyncQueueEntry.find_by!(tournament: tournament) entry = TournamentSyncQueueEntry.find_by!(tournament:)
allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed, 'network down') allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed,
'network down')
described_class.process_due! described_class.process_due!

View File

@ -22,7 +22,7 @@ module PerformanceHelpers
end end
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
{ result: result, query_count: query_count, duration_ms: duration_ms } { result:, query_count:, duration_ms: }
end end
def create_group_stage_tournament(groups_count:, teams_per_group:) def create_group_stage_tournament(groups_count:, teams_per_group:)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'ruby-prof' require 'ruby-prof'
RSpec.configure do |config| RSpec.configure do |config|

View File

@ -670,7 +670,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
@task @task
def lint(c): def lint(c):
"""Run RuboCop.""" """Run RuboCop."""
c.run(_bundle_exec("rubocop"), pty=True) c.run(_bundle_exec("rubocop", "--cache", "false"), pty=True)
@task(name="bootstrap-dev-user") @task(name="bootstrap-dev-user")