fix: clear RuboCop offenses
This commit is contained in:
parent
4bd1531408
commit
cfb07e82fc
|
|
@ -13,6 +13,21 @@ include:
|
|||
- project: 'turniere/turniere-infra'
|
||||
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:
|
||||
stage: test
|
||||
tags:
|
||||
|
|
|
|||
38
.rubocop.yml
38
.rubocop.yml
|
|
@ -4,6 +4,8 @@ require:
|
|||
|
||||
# Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56
|
||||
AllCops:
|
||||
NewCops: disable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- "bin/**/*"
|
||||
- "db/schema.rb"
|
||||
|
|
@ -13,15 +15,15 @@ Rails:
|
|||
Enabled: true
|
||||
|
||||
# Commonly used screens these days easily fit more than 80 characters.
|
||||
Metrics/LineLength:
|
||||
Max: 120
|
||||
Layout/LineLength:
|
||||
Max: 140
|
||||
|
||||
# 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
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- "db/migrate/*"
|
||||
Max: 50
|
||||
Max: 60
|
||||
|
||||
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
||||
Metrics/ClassLength:
|
||||
|
|
@ -33,16 +35,42 @@ Metrics/ModuleLength:
|
|||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
- "db/migrate/*"
|
||||
Max: 50
|
||||
Max: 75
|
||||
|
||||
Metrics/BlockLength:
|
||||
ExcludedMethods:
|
||||
Max: 30
|
||||
AllowedMethods:
|
||||
- "namespace"
|
||||
- "create_table"
|
||||
Exclude:
|
||||
- "config/**/*.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
|
||||
# should be done with yard instead
|
||||
Style/Documentation:
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -44,8 +44,8 @@ group :test, optional: true do
|
|||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'rspec-rails'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'ruby-prof'
|
||||
gem 'shoulda-matchers'
|
||||
end
|
||||
|
||||
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
|
||||
gem 'railroady'
|
||||
# Use sqlite3 as the database for Active Record
|
||||
gem 'sqlite3'
|
||||
gem 'rubocop'
|
||||
gem 'rubocop-performance'
|
||||
gem 'rubocop-rails'
|
||||
gem 'solargraph'
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||
gem 'sqlite3'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
|
|||
|
|
@ -47,9 +47,7 @@ class MatchesController < ApplicationController
|
|||
handle_match_end if new_state == 'finished'
|
||||
if @match.group_match? && new_state == 'in_progress'
|
||||
group = @match.group
|
||||
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
||||
logger.warn "Updating groups group score failed for #{group}"
|
||||
end
|
||||
logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||
end
|
||||
|
||||
render json: @match
|
||||
|
|
@ -66,7 +64,8 @@ class MatchesController < ApplicationController
|
|||
# PATCH /matches/1/swap
|
||||
def swap
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -83,7 +82,8 @@ class MatchesController < ApplicationController
|
|||
end
|
||||
|
||||
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)
|
||||
TournamentLiveBroadcast.call(synced_tournament.id)
|
||||
end
|
||||
|
|
@ -93,9 +93,7 @@ class MatchesController < ApplicationController
|
|||
def handle_match_end
|
||||
if @match.group_match?
|
||||
group = @match.group
|
||||
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
||||
logger.warn "Updating groups group score failed for #{group}"
|
||||
end
|
||||
logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||
end
|
||||
|
||||
return if @match.group_match?
|
||||
|
|
@ -116,11 +114,9 @@ class MatchesController < ApplicationController
|
|||
def validate_params
|
||||
case match_params['state']
|
||||
when 'in_progress'
|
||||
render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity \
|
||||
unless @match.not_started?
|
||||
render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity unless @match.not_started?
|
||||
when 'finished'
|
||||
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity \
|
||||
unless @match.in_progress?
|
||||
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity unless @match.in_progress?
|
||||
else
|
||||
render json: { error: 'Invalid target state' }, status: :unprocessable_entity
|
||||
end
|
||||
|
|
@ -162,11 +158,9 @@ class MatchesController < ApplicationController
|
|||
state: :not_started,
|
||||
position: (@group.matches.maximum(:position) || -1) + 1,
|
||||
decider: true,
|
||||
match_scores: teams.map { |team| MatchScore.new(team: team) }
|
||||
match_scores: teams.map { |team| MatchScore.new(team:) }
|
||||
).tap do |match|
|
||||
if team_ids.size != 2 || teams.size != 2
|
||||
match.errors.add(:team_ids, 'must contain exactly two teams from this group')
|
||||
end
|
||||
match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class StagesController < ApplicationController
|
|||
end
|
||||
|
||||
@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
|
||||
raise ActiveRecord::Rollback
|
||||
|
|
@ -61,7 +61,7 @@ class StagesController < ApplicationController
|
|||
if blocking_ties.any?
|
||||
render json: {
|
||||
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
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
|
|
|||
|
|
@ -70,11 +70,12 @@ class TournamentsController < ApplicationController
|
|||
end
|
||||
|
||||
raise ActionController::ParameterMissing, :teams if teams.nil?
|
||||
|
||||
if group_stage
|
||||
params.require(:playoff_teams_amount)
|
||||
groups = organize_teams_in_groups(teams)
|
||||
# add groups to tournament
|
||||
result = AddGroupStageToTournamentAndSave.call(tournament: tournament, groups: groups)
|
||||
result = AddGroupStageToTournamentAndSave.call(tournament:, groups:)
|
||||
# associate provided teams with tournament on success
|
||||
tournament.teams = groups.flatten if result.success?
|
||||
else
|
||||
|
|
@ -83,7 +84,7 @@ class TournamentsController < ApplicationController
|
|||
# associate provided teams with tournament
|
||||
tournament.teams = teams
|
||||
# add playoff stage to tournament
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: tournament.teams)
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: tournament.teams)
|
||||
end
|
||||
# validate tournament
|
||||
unless tournament.valid?
|
||||
|
|
@ -165,7 +166,6 @@ class TournamentsController < ApplicationController
|
|||
}, status: :bad_gateway
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def timer_params
|
||||
|
|
@ -253,7 +253,7 @@ class TournamentsController < ApplicationController
|
|||
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
||||
|
||||
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
|
||||
end
|
||||
|
|
@ -288,12 +288,16 @@ class TournamentsController < ApplicationController
|
|||
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def sync_state_params
|
||||
params.require(:snapshot).permit!.to_h
|
||||
end
|
||||
|
||||
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
|
||||
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
||||
|
|
@ -346,17 +350,19 @@ class TournamentsController < ApplicationController
|
|||
return
|
||||
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
|
||||
end
|
||||
|
||||
# throw error if both timestamp and timestamp_seconds are 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
|
||||
|
||||
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
|
||||
|
||||
if timestamp_seconds.present?
|
||||
|
|
@ -366,7 +372,10 @@ class TournamentsController < ApplicationController
|
|||
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
|
||||
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
|
||||
params[:timestamp] = parsed_time
|
||||
|
|
@ -377,22 +386,23 @@ class TournamentsController < ApplicationController
|
|||
if parsed_time.nil?
|
||||
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||
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
|
||||
|
||||
params[:timestamp] = parsed_time
|
||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||
rescue ArgumentError
|
||||
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||
render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||
end
|
||||
elsif timer_mode == 'countup'
|
||||
params[:timestamp] = Time.zone.now
|
||||
elsif timer_mode.present?
|
||||
return render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
|
||||
status: :unprocessable_entity
|
||||
render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
|
||||
status: :unprocessable_entity
|
||||
else
|
||||
return render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
|
||||
status: :unprocessable_entity
|
||||
render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ class AdvanceTeamsInIntermediateStage
|
|||
|
||||
# populate stage below with the "winners" from single team matches
|
||||
matches.select { |m| m.state == 'single_team' }
|
||||
.each do |match|
|
||||
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success?
|
||||
end
|
||||
.each do |match|
|
||||
context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
|
||||
end
|
||||
(context.object_to_save ||= []) << intermediate_stage
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Beamer < ApplicationRecord
|
|||
BeamerBroadcast.call(
|
||||
tournament_id: snapshot.fetch(:tournament_id),
|
||||
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
|
||||
@broadcast_previous_name = nil
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ class Stage < ApplicationRecord
|
|||
delegate :owner, to: :tournament
|
||||
|
||||
def teams
|
||||
if !matches.size.zero?
|
||||
if !matches.empty?
|
||||
matches.map(&:teams).flatten.uniq
|
||||
elsif !groups.size.zero?
|
||||
elsif !groups.empty?
|
||||
groups.map(&:teams).flatten.uniq
|
||||
else
|
||||
[]
|
||||
|
|
@ -20,9 +20,9 @@ class Stage < ApplicationRecord
|
|||
end
|
||||
|
||||
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?)
|
||||
return relevant_matches.find { |m| m.state != 'finished' }.nil?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
|
|||
|
||||
validates :name, :key, :action_name, :source, presence: true
|
||||
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
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
|
|||
team_action_items.where.not(team_id: desired_team_ids).destroy_all
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -80,14 +80,6 @@ class Tournament < ApplicationRecord
|
|||
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
|
||||
return if read_only_mode?
|
||||
return if sync_target_url.present?
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord
|
|||
def acquire_lock!
|
||||
token = SecureRandom.hex(8)
|
||||
updated = self.class
|
||||
.where(id: id)
|
||||
.where(id:)
|
||||
.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)
|
||||
return nil if updated.zero?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class TournamentSerializer < SimpleTournamentSerializer
|
|||
attribute :read_only_mode, if: :sync_metadata_visible?
|
||||
attribute :sync_target_url, if: :sync_metadata_visible?
|
||||
attribute :sync_last_push_error, if: :sync_metadata_visible?
|
||||
# NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against
|
||||
# NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against
|
||||
|
||||
has_many :stages
|
||||
has_many :team_action_lists
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
class BeamerBroadcastPayload
|
||||
class << self
|
||||
def build(beamer: nil, snapshot: nil, event:)
|
||||
def build(event:, beamer: nil, snapshot: nil)
|
||||
data = snapshot || self.snapshot(beamer)
|
||||
|
||||
{
|
||||
type: "beamer.#{event}",
|
||||
event: event,
|
||||
event:,
|
||||
tournament_id: data.fetch(:tournament_id),
|
||||
beamer: serialized_beamer(data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
class GroupStageService
|
||||
class << self
|
||||
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 '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 }
|
||||
Stage.new level: -1, groups: groups, state: :in_progress
|
||||
Stage.new level: -1, groups:, state: :in_progress
|
||||
end
|
||||
|
||||
def get_group_object_from(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
|
||||
|
||||
def deal_with_spacing_of_teams(matches, team_size)
|
||||
|
|
@ -71,7 +71,7 @@ class GroupStageService
|
|||
def update_group_scores(group)
|
||||
changed_group_scores = []
|
||||
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 }
|
||||
# reset previous values
|
||||
group_score.group_points = 0
|
||||
|
|
@ -217,7 +217,7 @@ class GroupStageService
|
|||
def comparison_partner_index(sorted_group_scores, index)
|
||||
return index - 1 if index.positive? && sorted_group_scores[index - 1].position == sorted_group_scores[index].position
|
||||
return index + 1 if index < sorted_group_scores.size - 1 &&
|
||||
sorted_group_scores[index + 1].position == sorted_group_scores[index].position
|
||||
sorted_group_scores[index + 1].position == sorted_group_scores[index].position
|
||||
return index - 1 if index.positive?
|
||||
return index + 1 if sorted_group_scores.size > 1
|
||||
|
||||
|
|
@ -239,11 +239,9 @@ class GroupStageService
|
|||
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
||||
|
||||
end_index = index
|
||||
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||
end_index += 1
|
||||
end
|
||||
end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||
|
||||
{ start_index: start_index, end_index: end_index }
|
||||
{ start_index:, end_index: }
|
||||
end
|
||||
|
||||
def advancing_slots_for_group(group)
|
||||
|
|
@ -272,7 +270,7 @@ class GroupStageService
|
|||
advancing_slots = advancing_slots_for_group(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
|
||||
|
||||
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
||||
|
|
@ -296,8 +294,9 @@ class GroupStageService
|
|||
decider_match_id: comparison_reason[:decider_match_id],
|
||||
tied: comparison_reason[:tied],
|
||||
needs_decider_match: group_ready_for_decider_check &&
|
||||
comparison_reason[:tied] &&
|
||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
|
||||
comparison_reason[:tied] &&
|
||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
|
||||
advancing_slots)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class PlayoffStageService
|
|||
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
||||
playoffs << initial_stage
|
||||
# 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
|
||||
end
|
||||
|
|
@ -25,7 +25,7 @@ class PlayoffStageService
|
|||
#
|
||||
# @param stage_count [Integer] number of stages to generate
|
||||
# @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 = []
|
||||
stage_count.times do |i|
|
||||
match_count = 2**i
|
||||
|
|
@ -82,7 +82,9 @@ class PlayoffStageService
|
|||
# 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
|
||||
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)
|
||||
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class TeamActionItemUpdate
|
||||
class << self
|
||||
def call(team_action_item:, completed:)
|
||||
new(team_action_item: team_action_item, completed: completed).call
|
||||
new(team_action_item:, completed:).call
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ class TeamActionItemUpdate
|
|||
end
|
||||
|
||||
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)
|
||||
true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class TournamentSnapshotImporter
|
|||
].freeze
|
||||
|
||||
def self.import!(tournament:, snapshot:)
|
||||
new(tournament: tournament, snapshot: snapshot).import!
|
||||
new(tournament:, snapshot:).import!
|
||||
end
|
||||
|
||||
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.each(&:destroy!)
|
||||
end
|
||||
|
||||
def parse_time(value)
|
||||
return nil if value.blank?
|
||||
|
||||
|
|
@ -234,6 +235,6 @@ class TournamentSnapshotImporter
|
|||
def code_taken_by_another_tournament?(code)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class TournamentSyncEnqueue
|
|||
return false unless tournament.sync_push_enabled?
|
||||
|
||||
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.status = 'pending'
|
||||
entry.attempts = 0
|
||||
|
|
|
|||
|
|
@ -27,11 +27,9 @@ class TournamentSyncPusher
|
|||
end
|
||||
|
||||
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}"
|
||||
end
|
||||
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
||||
|
||||
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
|
||||
true
|
||||
|
|
@ -53,7 +51,7 @@ class TournamentSyncPusher
|
|||
request['accept'] = 'application/json'
|
||||
request['content-type'] = 'application/json'
|
||||
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|
|
||||
timeout = http_timeout_seconds
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class UserService
|
|||
|
||||
def bet!(match, team)
|
||||
validate_bet! match, team
|
||||
@user.bets.create! match: match, team: team
|
||||
@user.bets.create! match:, team:
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||
|
||||
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Rails.application.configure do
|
|||
|
||||
# Enable/disable caching. By default caching is disabled.
|
||||
# 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.cache_store = :memory_store
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ Rails.application.configure do
|
|||
config.active_support.deprecation = :notify
|
||||
|
||||
# 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.
|
||||
# require 'syslog/logger'
|
||||
|
|
@ -87,5 +87,5 @@ Rails.application.configure do
|
|||
}
|
||||
|
||||
# Logging to STDOUT (running in docker by default)
|
||||
config.logger = Logger.new(STDOUT)
|
||||
config.logger = Logger.new($stdout)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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
|
||||
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
|
||||
# notations and behaviors.
|
||||
Rails.application.config.filter_parameters += [
|
||||
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
|
||||
Rails.application.config.filter_parameters += %i[
|
||||
passw secret token _key crypt salt certificate otp ssn
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file eases your Rails 7.0 framework defaults upgrade.
|
||||
|
|
|
|||
|
|
@ -8,10 +8,9 @@
|
|||
].each { |path| Spring.watch(path) }
|
||||
|
||||
Spring.after_fork do
|
||||
if ENV['DEBUGGER_STORED_RUBYLIB']
|
||||
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path|
|
||||
next unless path =~ /ruby-debug-ide/
|
||||
load path + '/ruby-debug-ide/multiprocess/starter.rb'
|
||||
end
|
||||
ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path|
|
||||
next unless path.include?('ruby-debug-ide')
|
||||
|
||||
load "#{path}/ruby-debug-ide/multiprocess/starter.rb"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20190112182829)
|
||||
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
||||
def up
|
||||
return unless table_exists?(:active_storage_blobs)
|
||||
|
||||
unless column_exists?(:active_storage_blobs, :service_name)
|
||||
add_column :active_storage_blobs, :service_name, :string
|
||||
return if column_exists?(:active_storage_blobs, :service_name)
|
||||
|
||||
if configured_service = ActiveStorage::Blob.service.name
|
||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||
end
|
||||
add_column :active_storage_blobs, :service_name, :string
|
||||
|
||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||
if (configured_service = ActiveStorage::Blob.service.name)
|
||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||
end
|
||||
|
||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20191206030411)
|
||||
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
|
|
@ -8,20 +10,21 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
|||
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_key_type
|
||||
config = Rails.configuration.generators
|
||||
config.options[config.orm][:primary_key_type] || :primary_key
|
||||
end
|
||||
|
||||
def blobs_primary_key_type
|
||||
pkey_name = connection.primary_key(:active_storage_blobs)
|
||||
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
|
||||
pkey_column.bigint? ? :bigint : pkey_column.type
|
||||
end
|
||||
def primary_key_type
|
||||
config = Rails.configuration.generators
|
||||
config.options[config.orm][:primary_key_type] || :primary_key
|
||||
end
|
||||
|
||||
def blobs_primary_key_type
|
||||
pkey_name = connection.primary_key(:active_storage_blobs)
|
||||
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
|
||||
pkey_column.bigint? ? :bigint : pkey_column.type
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20211119233751)
|
||||
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :group_scores, :position, :integer
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
change_column_default :group_scores, :position, 0
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tournaments, :timer_end, :datetime
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
|
|||
t.timestamps
|
||||
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|
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tournaments, :timer_mode, :string
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module TurniereE2E
|
|||
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
|
||||
|
||||
def initialize(base_url:)
|
||||
@base_url = base_url.sub(%r{/\z}, '')
|
||||
@base_url = base_url.delete_suffix('/')
|
||||
@messages = Queue.new
|
||||
@closed = false
|
||||
end
|
||||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
|||
def subscribe_team_action_lists!(tournament_id:)
|
||||
identifier = {
|
||||
channel: 'TournamentTeamActionListsChannel',
|
||||
tournament_id: tournament_id
|
||||
tournament_id:
|
||||
}.to_json
|
||||
|
||||
subscribe!(identifier:) do |message|
|
||||
|
|
@ -30,7 +30,7 @@ module TurniereE2E
|
|||
def subscribe_timer!(tournament_id:)
|
||||
identifier = {
|
||||
channel: 'TournamentTimersChannel',
|
||||
tournament_id: tournament_id
|
||||
tournament_id:
|
||||
}.to_json
|
||||
|
||||
subscribe!(identifier:) do |message|
|
||||
|
|
@ -41,8 +41,8 @@ module TurniereE2E
|
|||
def subscribe_beamer!(tournament_id:, beamer_name:)
|
||||
identifier = {
|
||||
channel: 'TournamentBeamersChannel',
|
||||
tournament_id: tournament_id,
|
||||
beamer_name: beamer_name
|
||||
tournament_id:,
|
||||
beamer_name:
|
||||
}.to_json
|
||||
|
||||
subscribe!(identifier:) do |message|
|
||||
|
|
@ -74,31 +74,31 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def wait_for_payload!(timeout: 20)
|
||||
wait_for!(timeout: timeout) do |message|
|
||||
wait_for!(timeout:) do |message|
|
||||
team_action_lists_payload_message?(message)
|
||||
end.fetch('message')
|
||||
end
|
||||
|
||||
def wait_for_timer_payload!(timeout: 20)
|
||||
wait_for!(timeout: timeout) do |message|
|
||||
wait_for!(timeout:) do |message|
|
||||
timer_payload_message?(message)
|
||||
end.fetch('message')
|
||||
end
|
||||
|
||||
def wait_for_beamer_payload!(timeout: 20)
|
||||
wait_for!(timeout: timeout) do |message|
|
||||
wait_for!(timeout:) do |message|
|
||||
beamer_payload_message?(message)
|
||||
end.fetch('message')
|
||||
end
|
||||
|
||||
def wait_for_tournament_payload!(timeout: 20)
|
||||
wait_for!(timeout: timeout) do |message|
|
||||
wait_for!(timeout:) do |message|
|
||||
tournament_payload_message?(message)
|
||||
end.fetch('message')
|
||||
end
|
||||
|
||||
def wait_for_matches_payload!(timeout: 20)
|
||||
wait_for!(timeout: timeout) do |message|
|
||||
wait_for!(timeout:) do |message|
|
||||
matches_payload_message?(message)
|
||||
end.fetch('message')
|
||||
end
|
||||
|
|
@ -117,9 +117,7 @@ module TurniereE2E
|
|||
websocket_url
|
||||
end
|
||||
|
||||
def write(data)
|
||||
socket.write(data)
|
||||
end
|
||||
delegate :write, to: :socket
|
||||
|
||||
private
|
||||
|
||||
|
|
@ -131,7 +129,7 @@ module TurniereE2E
|
|||
driver.text(
|
||||
JSON.dump(
|
||||
command: 'subscribe',
|
||||
identifier: identifier
|
||||
identifier:
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -180,8 +178,8 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def pump_until_open!(timeout: 10)
|
||||
deadline = Time.now + timeout
|
||||
while Time.now < deadline
|
||||
deadline = Time.zone.now + timeout
|
||||
while Time.zone.now < deadline
|
||||
read_from_socket(timeout: 0.5)
|
||||
return if driver.ready_state == 1
|
||||
end
|
||||
|
|
@ -190,14 +188,14 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def wait_for!(timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
loop do
|
||||
until @messages.empty?
|
||||
message = @messages.pop(true)
|
||||
return message if yield(message)
|
||||
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)
|
||||
rescue ThreadError
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
|||
attr_reader :base_url
|
||||
|
||||
def initialize(base_url:)
|
||||
@base_url = base_url.sub(%r{/\z}, '')
|
||||
@base_url = base_url.delete_suffix('/')
|
||||
@auth_headers = {}
|
||||
@login_credentials = nil
|
||||
end
|
||||
|
|
@ -29,10 +29,10 @@ module TurniereE2E
|
|||
:post,
|
||||
'/users',
|
||||
body: {
|
||||
email: email,
|
||||
password: password,
|
||||
email:,
|
||||
password:,
|
||||
password_confirmation: password,
|
||||
username: username
|
||||
username:
|
||||
}
|
||||
)
|
||||
store_auth_headers!(response)
|
||||
|
|
@ -40,14 +40,14 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def login!(email:, password:)
|
||||
@login_credentials = { email: email, password: password }
|
||||
@login_credentials = { email:, password: }
|
||||
@auth_headers = {}
|
||||
response = request(
|
||||
:post,
|
||||
'/users/sign_in',
|
||||
body: {
|
||||
email: email,
|
||||
password: password
|
||||
email:,
|
||||
password:
|
||||
}
|
||||
)
|
||||
store_auth_headers!(response)
|
||||
|
|
@ -59,19 +59,19 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def get(path, params: nil, headers: {})
|
||||
request(:get, path, params: params, headers: headers)
|
||||
request(:get, path, params:, headers:)
|
||||
end
|
||||
|
||||
def post(path, body: nil, params: nil, headers: {})
|
||||
request(:post, path, body: body, params: params, headers: headers)
|
||||
request(:post, path, body:, params:, headers:)
|
||||
end
|
||||
|
||||
def patch(path, body: nil, params: nil, headers: {})
|
||||
request(:patch, path, body: body, params: params, headers: headers)
|
||||
request(:patch, path, body:, params:, headers:)
|
||||
end
|
||||
|
||||
def delete(path, params: nil, headers: {})
|
||||
request(:delete, path, params: params, headers: headers)
|
||||
request(:delete, path, params:, headers:)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -80,7 +80,7 @@ module TurniereE2E
|
|||
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, 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
|
||||
when :get then Net::HTTP::Get
|
||||
|
|
@ -108,7 +108,8 @@ module TurniereE2E
|
|||
store_auth_headers!(result)
|
||||
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
|
||||
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
|
||||
result
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ require 'uri'
|
|||
|
||||
module TurniereE2E
|
||||
class ScenarioRunner
|
||||
DEFAULT_PASSWORD = 'password123'.freeze
|
||||
DEFAULT_PASSWORD = 'password123'
|
||||
|
||||
attr_reader :client
|
||||
|
||||
def initialize(base_url:, email: nil, password: nil, username: nil)
|
||||
@client = ApiClient.new(base_url: base_url)
|
||||
@client = ApiClient.new(base_url:)
|
||||
@email = email
|
||||
@password = password
|
||||
@username = username
|
||||
|
|
@ -33,19 +33,28 @@ module TurniereE2E
|
|||
body: { timestamp_seconds: 600 }
|
||||
)
|
||||
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!(
|
||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||
params: { state: 'upcoming' }
|
||||
)
|
||||
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)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
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")
|
||||
checkpoints[:statistics] = {
|
||||
|
|
@ -57,21 +66,27 @@ module TurniereE2E
|
|||
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
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))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
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)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:final_finish_attempted] = {
|
||||
finish_attempt: finish_attempt,
|
||||
finish_attempt:,
|
||||
tournament: summarize_tournament(tournament)
|
||||
}
|
||||
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
result(session:, tournament:, checkpoints:, stop_at:)
|
||||
end
|
||||
|
||||
def run_group_stage_intermediate_round(stop_at: :final_finish_attempted)
|
||||
|
|
@ -83,42 +98,54 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
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_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
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))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
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)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:final_finish_attempted] = {
|
||||
finish_attempt: finish_attempt,
|
||||
finish_attempt:,
|
||||
tournament: summarize_tournament(tournament)
|
||||
}
|
||||
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
result(session:, tournament:, checkpoints:, stop_at:)
|
||||
end
|
||||
|
||||
def run_playoff_only(team_count:, stop_at: :completed)
|
||||
session = register_session!
|
||||
tournament = create_tournament!(create_playoff_only_payload(team_count: team_count))
|
||||
tournament = create_tournament!(create_playoff_only_payload(team_count:))
|
||||
checkpoints = {
|
||||
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))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:completed] = summarize_tournament(tournament)
|
||||
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
result(session:, tournament:, checkpoints:, stop_at:)
|
||||
end
|
||||
|
||||
def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores)
|
||||
|
|
@ -130,7 +157,10 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
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
|
||||
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))
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
result(session:, tournament:, checkpoints:, stop_at:)
|
||||
end
|
||||
|
||||
def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show)
|
||||
session = register_session!
|
||||
tournament = create_tournament!(
|
||||
create_group_stage_payload(
|
||||
group_count: group_count,
|
||||
teams_per_group: teams_per_group,
|
||||
playoff_teams_amount: playoff_teams_amount
|
||||
group_count:,
|
||||
teams_per_group:,
|
||||
playoff_teams_amount:
|
||||
)
|
||||
)
|
||||
|
||||
checkpoints = {
|
||||
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))
|
||||
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
|
||||
|
||||
def wait_for_healthcheck!(timeout_seconds: 30)
|
||||
started_at = Time.now
|
||||
started_at = Time.zone.now
|
||||
loop do
|
||||
begin
|
||||
response = client.get('/healthz')
|
||||
|
|
@ -176,7 +209,7 @@ module TurniereE2E
|
|||
nil
|
||||
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
|
||||
end
|
||||
|
|
@ -190,11 +223,11 @@ module TurniereE2E
|
|||
unique = SecureRandom.hex(6)
|
||||
email = "e2e-#{unique}@example.com"
|
||||
username = "e2e-#{unique}"
|
||||
response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
||||
return { email: email, username: username } if client.authenticated?
|
||||
client.register!(email:, password: DEFAULT_PASSWORD, username:)
|
||||
return { email:, username: } if client.authenticated?
|
||||
|
||||
login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
|
||||
return { email: email, username: username } if login_response.fetch(:status) == 200 && client.authenticated?
|
||||
login_response = client.login!(email:, password: DEFAULT_PASSWORD)
|
||||
return { email:, username: } if login_response.fetch(:status) == 200 && client.authenticated?
|
||||
|
||||
raise ApiError.new(
|
||||
'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD',
|
||||
|
|
@ -216,19 +249,19 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def get!(path, params: nil)
|
||||
response = get_response!(path, params: params)
|
||||
response = get_response!(path, params:)
|
||||
expect_status!(response, 200, "get #{path}")
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
||||
def get_response!(path, params: nil)
|
||||
response = client.get(path, params: params)
|
||||
response = client.get(path, params:)
|
||||
expect_status!(response, 200, "get #{path}")
|
||||
response
|
||||
end
|
||||
|
||||
def patch!(path, body:)
|
||||
response = client.patch(path, body: body)
|
||||
response = client.patch(path, body:)
|
||||
expect_status!(response, 200, "patch #{path}")
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
|
@ -244,9 +277,12 @@ module TurniereE2E
|
|||
def run_playoffs_until_before_final!(tournament_id)
|
||||
loop do
|
||||
tournament = fetch_tournament!(tournament_id)
|
||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 }
|
||||
.select { |match| match.fetch(:state) == 'not_started' }
|
||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
|
||||
.select { |match| match.fetch(:state) == 'not_started' }
|
||||
.sort_by do |match|
|
||||
[-match.fetch(:stage_level),
|
||||
match.fetch(:position)]
|
||||
end
|
||||
|
||||
break if playable_matches.empty?
|
||||
|
||||
|
|
@ -260,7 +296,10 @@ module TurniereE2E
|
|||
loop do
|
||||
tournament = fetch_tournament!(tournament_id)
|
||||
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?
|
||||
|
||||
playable_matches.each do |match|
|
||||
|
|
@ -276,7 +315,7 @@ module TurniereE2E
|
|||
refreshed_match = fetch_match!(match.fetch(:id))
|
||||
refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score|
|
||||
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)}")
|
||||
end
|
||||
|
||||
|
|
@ -284,7 +323,9 @@ module TurniereE2E
|
|||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||
if expected_finish_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
|
||||
|
||||
{
|
||||
|
|
@ -304,7 +345,7 @@ module TurniereE2E
|
|||
points = points_by_team_name.fetch(team_name) do
|
||||
raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}"
|
||||
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)}")
|
||||
end
|
||||
|
||||
|
|
@ -312,7 +353,9 @@ module TurniereE2E
|
|||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||
if expected_finish_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
|
||||
|
||||
{
|
||||
|
|
@ -339,7 +382,9 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
||||
|
|
@ -354,7 +399,9 @@ module TurniereE2E
|
|||
|
||||
{
|
||||
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
|
||||
|
||||
|
|
@ -384,7 +431,7 @@ module TurniereE2E
|
|||
match = matches_by_teams.fetch(team_names.sort) do
|
||||
raise "match for #{team_names.inspect} not found"
|
||||
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
|
||||
|
||||
|
|
@ -404,7 +451,7 @@ module TurniereE2E
|
|||
|
||||
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
||||
teams = group_count.times.flat_map do |group_index|
|
||||
teams_per_group.times.map do |seed_index|
|
||||
Array.new(teams_per_group) do |seed_index|
|
||||
{
|
||||
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
||||
group: group_index
|
||||
|
|
@ -417,8 +464,8 @@ module TurniereE2E
|
|||
description: 'HTTP lifecycle E2E tournament',
|
||||
public: false,
|
||||
group_stage: true,
|
||||
playoff_teams_amount: playoff_teams_amount,
|
||||
teams: teams
|
||||
playoff_teams_amount:,
|
||||
teams:
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -427,7 +474,7 @@ module TurniereE2E
|
|||
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
||||
description: 'HTTP lifecycle E2E tournament',
|
||||
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
|
||||
|
||||
|
|
@ -527,7 +574,7 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def parse_server_timing(header)
|
||||
return {} if header.nil? || header.empty?
|
||||
return {} if header.blank?
|
||||
|
||||
header.split(',').each_with_object({}) do |entry, sections|
|
||||
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
||||
|
|
@ -539,10 +586,10 @@ module TurniereE2E
|
|||
|
||||
def result(session:, tournament:, checkpoints:, stop_at:)
|
||||
{
|
||||
session: session,
|
||||
session:,
|
||||
stop_at: stop_at.to_s,
|
||||
tournament: summarize_tournament(tournament),
|
||||
checkpoints: checkpoints
|
||||
checkpoints:
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -557,10 +604,14 @@ module TurniereE2E
|
|||
end
|
||||
end
|
||||
end
|
||||
def login_with_existing_credentials!
|
||||
login_response = client.login!(email: @email, password: @password)
|
||||
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?
|
||||
|
||||
{ email: @email, username: @username || @email }
|
||||
end
|
||||
def login_with_existing_credentials!
|
||||
login_response = client.login!(email: @email, password: @password)
|
||||
expect_status!(login_response, 200, "login #{@email}")
|
||||
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 }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module Local
|
|||
result = yield
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ options = {
|
|||
group_count: 4,
|
||||
teams_per_group: 4,
|
||||
playoff_teams_amount: 8,
|
||||
email: ENV['TURNIERE_E2E_EMAIL'],
|
||||
password: ENV['TURNIERE_E2E_PASSWORD'],
|
||||
username: ENV['TURNIERE_E2E_USERNAME']
|
||||
email: ENV.fetch('TURNIERE_E2E_EMAIL', nil),
|
||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil),
|
||||
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||
}
|
||||
|
||||
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('--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('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value }
|
||||
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value }
|
||||
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value|
|
||||
options[:team_count] = 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|
|
||||
options[:playoff_teams_amount] = value
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ RSpec.describe BeamersController, type: :controller do
|
|||
before(:each) { apply_authentication_headers_for @owner }
|
||||
|
||||
it 'creates a new beamer' do
|
||||
expect {
|
||||
expect do
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -186,11 +186,11 @@ RSpec.describe BeamersController, type: :controller do
|
|||
before(:each) { apply_authentication_headers_for @owner }
|
||||
|
||||
it 'deletes the beamer' do
|
||||
expect {
|
||||
expect do
|
||||
delete :destroy, params: {
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,17 +24,18 @@ RSpec.describe GroupsController, type: :controller do
|
|||
|
||||
it 'returns ranking_decision metadata for group scores' do
|
||||
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.matches.destroy_all
|
||||
@group.group_scores.destroy_all
|
||||
|
||||
team_a = create(:team, tournament: tournament, name: 'A')
|
||||
team_b = create(:team, tournament: tournament, name: 'B')
|
||||
team_a = create(:team, tournament:, name: 'A')
|
||||
team_b = create(:team, tournament:, name: 'B')
|
||||
|
||||
create(:group_match, group: @group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 3)
|
||||
create(:match_score, match: match, team: team_b, points: 1)
|
||||
create(:match_score, match:, team: team_a, points: 3)
|
||||
create(:match_score, match:, team: team_b, points: 1)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ RSpec.describe MatchesController, type: :controller do
|
|||
expect(match[:teams]).to all(include(:id, :name))
|
||||
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
|
||||
|
||||
describe 'GET #show' do
|
||||
|
|
@ -65,12 +65,14 @@ RSpec.describe MatchesController, type: :controller do
|
|||
get :show, params: { id: @match.to_param }
|
||||
body = deserialize_response response
|
||||
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
|
||||
|
||||
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(: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
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
body = deserialize_response(response)
|
||||
expect(body.map { |match| match[: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(:id)).to match_array([first_match.id, last_match.id])
|
||||
expect(body.pluck(:position)).to match_array([original_first_position, original_last_position])
|
||||
end
|
||||
|
||||
it 'rejects matches from different groups' do
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ RSpec.describe StagesController, type: :controller do
|
|||
match.save!
|
||||
end
|
||||
|
||||
UpdateGroupsGroupScoresAndSave.call(group: group)
|
||||
UpdateGroupsGroupScoresAndSave.call(group:)
|
||||
end
|
||||
|
||||
stage.reload
|
||||
|
|
@ -179,17 +179,20 @@ RSpec.describe StagesController, type: :controller do
|
|||
|
||||
before do
|
||||
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_stage.groups = [group]
|
||||
team_a = create(:team, tournament: tournament, name: 'A')
|
||||
team_b = create(:team, tournament: tournament, name: 'B')
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
team_a = create(:team, tournament:, name: 'A')
|
||||
team_b = create(:team, tournament:, name: 'B')
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
score_a = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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)
|
||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
||||
|
||||
apply_authentication_headers_for group_stage.owner
|
||||
|
|
@ -215,17 +218,20 @@ RSpec.describe StagesController, type: :controller do
|
|||
|
||||
before do
|
||||
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_stage.groups = [group]
|
||||
team_a = create(:team, tournament: tournament, name: 'A')
|
||||
team_b = create(:team, tournament: tournament, name: 'B')
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
team_a = create(:team, tournament:, name: 'A')
|
||||
team_b = create(:team, tournament:, name: 'B')
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
score_a = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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)
|
||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
||||
|
||||
apply_authentication_headers_for group_stage.owner
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
get :index
|
||||
tournaments = deserialize_response response
|
||||
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
|
||||
|
||||
|
|
@ -35,15 +35,15 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
|
||||
it 'returns all private tournaments' do
|
||||
apply_authentication_headers_for @another_user
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
tournaments = deserialize_response response
|
||||
private_tournaments = Tournament.where(owner: @another_user, public: false).map { |t| t[:id] }
|
||||
returned_private_tournaments = tournaments.filter { |t| !t[:public] }.map { |t| t[:id] }
|
||||
private_tournaments = Tournament.where(owner: @another_user, public: false).pluck(:id)
|
||||
returned_private_tournaments = tournaments.filter { |t| !t[:public] }.pluck(:id)
|
||||
expect(returned_private_tournaments).to match_array(private_tournaments)
|
||||
end
|
||||
|
||||
it 'returns no private tournaments for unauthenticated users' do
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
tournaments = deserialize_response response
|
||||
private_tournaments = tournaments.reject { |t| t[:public] }
|
||||
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
|
||||
apply_authentication_headers_for @user
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
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
|
||||
|
||||
it 'returns no public tournaments' do
|
||||
apply_authentication_headers_for @another_user
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
tournaments = deserialize_response response
|
||||
expect(tournaments.count { |t| t[:public] }).to eq(0)
|
||||
end
|
||||
|
|
@ -70,15 +70,15 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
end
|
||||
|
||||
it 'returns all public tournaments' do
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
tournaments = deserialize_response response
|
||||
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
|
||||
|
||||
it 'returns no private tournaments' do
|
||||
apply_authentication_headers_for @another_user
|
||||
get :index, params: params
|
||||
get(:index, params:)
|
||||
tournaments = deserialize_response response
|
||||
expect(tournaments.count { |t| !t[:public] }).to eq(0)
|
||||
end
|
||||
|
|
@ -224,7 +224,7 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
describe 'GET #timer' do
|
||||
it 'returns timer state in ISO 8601 format' do
|
||||
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 }
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ RSpec.describe TournamentsController, type: :controller do
|
|||
it 'stores timezone-aware timestamp values without shifting them' do
|
||||
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(@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(@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')
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
|
||||
let(:owner_client) { login_client(email: owner_email, password: owner_password) }
|
||||
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
|
||||
response = anonymous_client.get('/healthz')
|
||||
|
|
@ -34,14 +34,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
email = "http-e2e-#{unique}@example.com"
|
||||
password = 'password123'
|
||||
username = "http-e2e-#{unique}"
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
client = TurniereE2E::ApiClient.new(base_url:)
|
||||
registration = client.post('/users', body: {
|
||||
email: email,
|
||||
password: password,
|
||||
email:,
|
||||
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(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' })
|
||||
|
||||
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].map { |tournament| tournament[:id] }).not_to include(private_tournament.fetch(:id))
|
||||
expect(public_index[:json].pluck(:id)).to include(public_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[:json].map { |tournament| tournament[: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)).to include(private_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[:json][:id]).to eq(public_tournament.fetch(:id))
|
||||
|
|
@ -93,10 +93,10 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
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(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(invalid_teams[:status]).to eq(422)
|
||||
|
|
@ -133,7 +133,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
timestamp_seconds: 120,
|
||||
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_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: {
|
||||
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")
|
||||
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_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||
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)}")
|
||||
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' })
|
||||
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' })
|
||||
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' })
|
||||
|
|
@ -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_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 })
|
||||
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) })
|
||||
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")
|
||||
|
|
@ -268,9 +273,9 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
expect(matches_all[:status]).to eq(200)
|
||||
expect(matches_all[:json].count).to eq(tournament_match_count(tournament))
|
||||
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[: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(invalid_match_update[:status]).to eq(422)
|
||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||
|
|
@ -332,35 +337,42 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
})
|
||||
swapped_tournament = fetch_tournament(client: owner_client, tournament_id: tournament.fetch(:id))
|
||||
swapped_group = swapped_tournament.fetch(:stages)
|
||||
.find { |stage| stage.fetch(:level) == -1 }
|
||||
.fetch(:groups)
|
||||
.find { |item| item.fetch(:id) == group.fetch(:id) }
|
||||
.find { |stage| stage.fetch(:level) == -1 }
|
||||
.fetch(:groups)
|
||||
.find { |item| item.fetch(:id) == group.fetch(:id) }
|
||||
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))
|
||||
|
||||
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_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[: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(: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_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_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
|
||||
|
||||
def login_client(email:, password:)
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: email, password: password)
|
||||
client = TurniereE2E::ApiClient.new(base_url:)
|
||||
response = client.login!(email:, password:)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
expect(client.authenticated?).to eq(true)
|
||||
|
|
@ -368,15 +380,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
end
|
||||
|
||||
def secondary_client
|
||||
alt_email = ENV['TURNIERE_E2E_ALT_EMAIL']
|
||||
alt_password = ENV['TURNIERE_E2E_ALT_PASSWORD']
|
||||
alt_email = ENV.fetch('TURNIERE_E2E_ALT_EMAIL', nil)
|
||||
alt_password = ENV.fetch('TURNIERE_E2E_ALT_PASSWORD', nil)
|
||||
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)
|
||||
return client if response[:status] == 200 && client.authenticated?
|
||||
end
|
||||
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
client = TurniereE2E::ApiClient.new(base_url:)
|
||||
unique = unique_suffix
|
||||
response = client.post('/users', body: {
|
||||
email: "http-e2e-secondary-#{unique}@example.com",
|
||||
|
|
@ -394,11 +406,11 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'HTTP API surface E2E group-stage tournament',
|
||||
public: public,
|
||||
public:,
|
||||
group_stage: true,
|
||||
playoff_teams_amount: 8,
|
||||
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}",
|
||||
group: group_index
|
||||
|
|
@ -409,22 +421,22 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
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
|
||||
|
||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
||||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'HTTP API surface E2E playoff tournament',
|
||||
public: public,
|
||||
teams: 4.times.map do |index|
|
||||
public:,
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||
end
|
||||
}
|
||||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
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
|
||||
|
||||
def fetch_tournament(client:, tournament_id:)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
|||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||
description: 'Websocket beamer tournament',
|
||||
public: true,
|
||||
teams: 4.times.map do |index|
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||
end
|
||||
})
|
||||
|
|
@ -89,13 +89,13 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
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 &&
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
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!
|
||||
|
||||
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: {
|
||||
display_state: 'groups'
|
||||
})
|
||||
|
|
@ -71,7 +73,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
|||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||
description: 'Follower beamer websocket source tournament',
|
||||
public: true,
|
||||
teams: 4.times.map do |index|
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||
end
|
||||
})
|
||||
|
|
@ -107,25 +109,25 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_default_beamer!(tournament_id:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
loop do
|
||||
beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_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
|
||||
end
|
||||
end
|
||||
|
||||
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
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 &&
|
||||
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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||
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(: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(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
||||
|
||||
|
|
@ -39,27 +41,28 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
|
||||
log_phase!('configure sync and verify initial replication')
|
||||
configure_sync = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||
sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state",
|
||||
sync_auth_token: sync_token
|
||||
})
|
||||
sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state",
|
||||
sync_auth_token: sync_token
|
||||
})
|
||||
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))
|
||||
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(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')
|
||||
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]
|
||||
first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||
first_position_match = first_group_matches.fetch(0)
|
||||
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 }
|
||||
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|
|
||||
match_team_names(match) == match_team_names(first_position_match)
|
||||
end
|
||||
|
|
@ -72,21 +75,24 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
})
|
||||
expect(leader_swap[:status]).to eq(200)
|
||||
expect(leader_swap[:json].map { |match| match.fetch(:position) }).to match_array([
|
||||
first_position_match.fetch(:position),
|
||||
fourth_position_match.fetch(:position)
|
||||
])
|
||||
first_position_match.fetch(:position),
|
||||
fourth_position_match.fetch(:position)
|
||||
])
|
||||
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(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq(
|
||||
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(
|
||||
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(
|
||||
first_position_match.fetch(:position)
|
||||
)
|
||||
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)
|
||||
)
|
||||
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)
|
||||
)
|
||||
|
||||
follower_before_invalid_swap = tournament_signature(follower)
|
||||
source_before_invalid_swap = tournament_signature(source)
|
||||
|
|
@ -97,7 +103,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
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')
|
||||
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(follower_after_invalid_swap)).to eq(follower_before_invalid_swap)
|
||||
|
||||
|
|
@ -111,15 +118,15 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
|
||||
log_phase!('tighten playoff cutoff and rename teams')
|
||||
update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||
playoff_teams_amount: 4,
|
||||
instant_finalists_amount: 4,
|
||||
intermediate_round_participants_amount: 0
|
||||
})
|
||||
playoff_teams_amount: 4,
|
||||
instant_finalists_amount: 4,
|
||||
intermediate_round_participants_amount: 0
|
||||
})
|
||||
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))
|
||||
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
rename_source_tournament_teams!(
|
||||
source_tournament_id: source.fetch(:id),
|
||||
team_names: [
|
||||
|
|
@ -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)
|
||||
]
|
||||
)
|
||||
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise', range: 2..3))
|
||||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', range: 3..4))
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise',
|
||||
range: 2..3))
|
||||
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')
|
||||
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) })
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by do |group|
|
||||
group.fetch(:number)
|
||||
end)
|
||||
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||
|
||||
log_phase!('verify playoff generation replicated')
|
||||
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(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(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)
|
||||
|
||||
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(follower)).not_to be_nil
|
||||
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')
|
||||
source_token_item = playoff_token_list(source).fetch(:team_action_items).first
|
||||
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 }
|
||||
)
|
||||
expect(source_token_update[:status]).to eq(200)
|
||||
|
||||
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?(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))
|
||||
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 }
|
||||
)
|
||||
expect(locked_token_update[:status]).to eq(423)
|
||||
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))
|
||||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', range: 2..5))
|
||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
||||
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise',
|
||||
range: 2..4))
|
||||
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')
|
||||
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')
|
||||
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(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)
|
||||
|
||||
follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id))
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -220,8 +243,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
end
|
||||
|
||||
def login_client(base_url:, email:, password:)
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: email, password: password)
|
||||
client = TurniereE2E::ApiClient.new(base_url:)
|
||||
response = client.login!(email:, password:)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
expect(client.authenticated?).to eq(true)
|
||||
|
|
@ -232,11 +255,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'Follower sync source tournament',
|
||||
public: public,
|
||||
public:,
|
||||
group_stage: true,
|
||||
playoff_teams_amount: 8,
|
||||
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}",
|
||||
group: group_index
|
||||
|
|
@ -247,34 +270,34 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
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
|
||||
|
||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
||||
payload = {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'Unrelated playoff tournament',
|
||||
public: public,
|
||||
teams: 4.times.map do |index|
|
||||
public:,
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||
end
|
||||
}
|
||||
|
||||
response = client.post('/tournaments', body: payload)
|
||||
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
|
||||
|
||||
def create_follower_tournament(client:, public:, name_prefix:)
|
||||
response = client.post('/tournaments', body: {
|
||||
name: "#{name_prefix} #{unique_suffix}",
|
||||
description: 'Follower tournament',
|
||||
public: public,
|
||||
public:,
|
||||
read_only_mode: true,
|
||||
sync_auth_token: sync_token
|
||||
})
|
||||
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
|
||||
|
||||
def fetch_tournament(client:, tournament_id:)
|
||||
|
|
@ -298,9 +321,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
amount = rand(range)
|
||||
Array.new(amount) do |index|
|
||||
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
|
||||
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
|
||||
|
|
@ -316,7 +339,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name })
|
||||
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
|
||||
return if team.nil?
|
||||
|
||||
|
|
@ -340,21 +363,21 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
last_progress_at = Time.now
|
||||
deadline = Time.zone.now + timeout
|
||||
last_progress_at = Time.zone.now
|
||||
|
||||
loop do
|
||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id)
|
||||
follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id)
|
||||
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}"
|
||||
$stdout.flush
|
||||
last_progress_at = Time.now
|
||||
last_progress_at = Time.zone.now
|
||||
end
|
||||
|
||||
if Time.now >= deadline
|
||||
if Time.zone.now >= deadline
|
||||
raise <<~ERROR
|
||||
Follower did not catch up within #{timeout}s
|
||||
source_signature=#{tournament_signature(source).inspect}
|
||||
|
|
@ -374,8 +397,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
end
|
||||
|
||||
def synced_team_id_pairs(source:, follower:)
|
||||
source_by_name = source.fetch(:teams).each_with_object({}) do |team, teams_by_name|
|
||||
teams_by_name[team.fetch(:name)] = team
|
||||
source_by_name = source.fetch(:teams).index_by do |team|
|
||||
team.fetch(:name)
|
||||
end
|
||||
follower.fetch(:teams).filter_map do |team|
|
||||
source_team = source_by_name[team.fetch(:name)]
|
||||
|
|
@ -396,17 +419,18 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
end
|
||||
|
||||
group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source_id)
|
||||
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
|
||||
stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||
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
|
||||
|
||||
stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index|
|
||||
decider_create = source_owner_client.post("/groups/#{blocking_tie.fetch(:group_id)}/matches", body: {
|
||||
team_ids: blocking_tie.fetch(:team_ids)
|
||||
})
|
||||
team_ids: blocking_tie.fetch(:team_ids)
|
||||
})
|
||||
expect(decider_create[:status]).to eq(201)
|
||||
decider_match = decider_create.fetch(:json)
|
||||
|
||||
|
|
@ -422,7 +446,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
expect(finish_decider[:status]).to eq(200)
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -464,10 +489,13 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
loop do
|
||||
tournament = fetch_tournament(client: source_anonymous_client, tournament_id: source_id)
|
||||
playoff_matches = tournament.fetch(:stages)
|
||||
.select { |stage| stage.fetch(:level) >= 0 }
|
||||
.flat_map { |stage| stage.fetch(:matches) }
|
||||
.select { |match| %w[not_started in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2 }
|
||||
.sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] }
|
||||
.select { |stage| stage.fetch(:level) >= 0 }
|
||||
.flat_map { |stage| stage.fetch(:matches) }
|
||||
.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)] }
|
||||
break if playoff_matches.empty?
|
||||
|
||||
playoff_matches.each_with_index do |match, index|
|
||||
|
|
@ -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) }
|
||||
[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)
|
||||
end
|
||||
|
||||
|
|
@ -504,7 +533,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
||||
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
||||
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|
|
||||
{
|
||||
key: list.fetch(:key),
|
||||
|
|
@ -596,6 +627,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
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')
|
||||
|
||||
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||
|
|
@ -61,7 +62,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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)
|
||||
expect(response[:status]).to eq(200)
|
||||
client
|
||||
|
|
@ -75,7 +76,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
|||
group_stage: true,
|
||||
playoff_teams_amount: 4,
|
||||
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}",
|
||||
group: group_index
|
||||
|
|
@ -84,7 +85,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
|||
end
|
||||
})
|
||||
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
|
||||
|
||||
def create_follower_tournament(name_prefix:)
|
||||
|
|
@ -101,7 +102,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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}")
|
||||
expect(response[:status]).to eq(200)
|
||||
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) }
|
||||
[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)
|
||||
end
|
||||
|
||||
|
|
@ -150,24 +151,24 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_list_presence!(tournament_id, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
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?
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max)
|
||||
return payload if team_action_item_completed?(payload, team_name: team_name) == completed
|
||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(:runner) do
|
||||
TurniereE2E::ScenarioRunner.new(
|
||||
base_url: base_url,
|
||||
base_url:,
|
||||
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
||||
username: ENV['TURNIERE_E2E_USERNAME']
|
||||
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16)
|
||||
|
||||
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)
|
||||
|
||||
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
||||
|
|
@ -49,8 +49,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
expect(created[:intermediate_round_participants_amount]).to eq(2)
|
||||
|
||||
playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
|
||||
.select { |stage| stage[:level] != -1 }
|
||||
.max_by { |stage| stage[:level] }
|
||||
.reject { |stage| stage[:level] == -1 }
|
||||
.max_by { |stage| stage[:level] }
|
||||
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('not_started')).to eq(1)
|
||||
|
|
@ -76,7 +76,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
expect(positions.fetch('G1-S2')).to eq(expected_positions[1])
|
||||
|
||||
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(
|
||||
team_name: 'G1-S1',
|
||||
position: 1,
|
||||
|
|
@ -85,7 +86,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
scored_points: 4,
|
||||
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(
|
||||
team_name: 'G1-S2',
|
||||
position: 1,
|
||||
|
|
@ -96,9 +98,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
)
|
||||
else
|
||||
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(
|
||||
team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2',
|
||||
position: 1,
|
||||
|
|
@ -107,7 +110,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
scored_points: 5,
|
||||
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(
|
||||
team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1',
|
||||
position: 2,
|
||||
|
|
@ -122,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
|||
|
||||
[2, 3, 5, 8].each do |team_count|
|
||||
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)
|
||||
final_stage = completed_stages.find { |stage| stage[:level] == 0 }
|
||||
|
|
|
|||
|
|
@ -15,17 +15,20 @@ RSpec.describe 'Tournament rendering HTTP E2E' do
|
|||
let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' }
|
||||
let(:runner) do
|
||||
TurniereE2E::ScenarioRunner.new(
|
||||
base_url: base_url,
|
||||
base_url:,
|
||||
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
||||
username: ENV['TURNIERE_E2E_USERNAME']
|
||||
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||
)
|
||||
end
|
||||
|
||||
[
|
||||
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, max_server_duration_ms: 1000 },
|
||||
{ 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 }
|
||||
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000,
|
||||
max_server_duration_ms: 1000 },
|
||||
{ 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|
|
||||
it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do
|
||||
result = runner.run_group_stage_render_profile(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
|||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||
let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" }
|
||||
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
|
||||
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)
|
||||
|
||||
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 }
|
||||
)
|
||||
expect(update_by_business_key[:status]).to eq(200)
|
||||
|
|
@ -118,7 +119,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
|
|
@ -133,7 +134,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
|||
group_stage: true,
|
||||
playoff_teams_amount: 4,
|
||||
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}",
|
||||
group: group_index
|
||||
|
|
@ -141,10 +142,10 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
|||
end
|
||||
end
|
||||
}
|
||||
body[:sync_target_url] = sync_target_url unless sync_target_url.nil? || sync_target_url.empty?
|
||||
body[:sync_auth_token] = sync_auth_token unless sync_auth_token.nil? || sync_auth_token.empty?
|
||||
body[:sync_target_url] = sync_target_url if sync_target_url.present?
|
||||
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)
|
||||
fetch_tournament(response.dig(:json, :id))
|
||||
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) }
|
||||
[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)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
|||
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
||||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||
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
|
||||
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))
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
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 }
|
||||
)
|
||||
expect(update_by_business_key[:status]).to eq(200)
|
||||
|
|
@ -67,7 +68,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
|
|
@ -82,7 +83,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
|||
group_stage: true,
|
||||
playoff_teams_amount: 4,
|
||||
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}",
|
||||
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) }
|
||||
[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)
|
||||
end
|
||||
|
||||
|
|
@ -143,12 +144,12 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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
|
||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max)
|
||||
return payload if team_action_item_completed?(payload, team_name: team_name) == completed
|
||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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)
|
||||
expect(response[:status]).to eq(200)
|
||||
client
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
|||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||
description: 'Follower websocket timer source tournament',
|
||||
public: false,
|
||||
teams: 4.times.map do |index|
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
||||
end
|
||||
})
|
||||
|
|
@ -96,12 +96,12 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
|||
it 'broadcasts timer state changes over websocket' do
|
||||
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))
|
||||
|
||||
initial_payload = cable_client.wait_for_timer_payload!
|
||||
|
|
@ -54,7 +54,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(response[:status]).to eq(200)
|
||||
|
|
@ -66,7 +66,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
|||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||
description: 'Websocket timer tournament',
|
||||
public: false,
|
||||
teams: 4.times.map do |index|
|
||||
teams: Array.new(4) do |index|
|
||||
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
||||
end
|
||||
})
|
||||
|
|
@ -75,12 +75,12 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
|||
end
|
||||
|
||||
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
||||
deadline = Time.now + timeout
|
||||
deadline = Time.zone.now + timeout
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ FactoryBot.define do
|
|||
name { Beamer::DEFAULT_NAME }
|
||||
|
||||
initialize_with do
|
||||
tournament.beamers.find_or_initialize_by(name: name)
|
||||
tournament.beamers.find_or_initialize_by(name:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ FactoryBot.define do
|
|||
stage
|
||||
|
||||
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|
|
||||
create(:group_score, team: team, group: group)
|
||||
create(:group_score, team:, group:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,17 +13,14 @@ FactoryBot.define do
|
|||
intermediate_round_participants_amount { 0 }
|
||||
end
|
||||
after(:create) do |tournament, evaluator|
|
||||
if evaluator.teams.present?
|
||||
tournament.teams = evaluator.teams
|
||||
else
|
||||
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
|
||||
end
|
||||
tournament.teams = (evaluator.teams.presence || create_list(:team, evaluator.teams_count, tournament:))
|
||||
tournament.playoff_teams_amount = evaluator.playoff_teams_amount
|
||||
tournament.instant_finalists_amount = evaluator.instant_finalists_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'
|
||||
end
|
||||
|
||||
tournament.save!
|
||||
end
|
||||
|
||||
|
|
@ -36,7 +33,7 @@ FactoryBot.define do
|
|||
(1..evaluator.stage_count).each do |level|
|
||||
tournament.stages << create(
|
||||
:playoff_stage,
|
||||
level: level,
|
||||
level:,
|
||||
match_count: -1,
|
||||
match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe AddGroupStageToTournament, type: :interactor do
|
|||
@empty_tournament = create(:stageless_tournament)
|
||||
@group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
|
||||
@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]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe Beamer, type: :model do
|
|||
end
|
||||
|
||||
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))
|
||||
expect(other).to be_valid
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,21 +15,21 @@ RSpec.describe GroupScore, type: :model do
|
|||
let(:team_c) { create(:team) }
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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(:match_score, match: match, team: team_a, points: 3)
|
||||
create(:match_score, match: match, team: team_c, points: 1)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 3)
|
||||
create(:match_score, match:, team: team_c, points: 1)
|
||||
end
|
||||
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 4)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 4)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
group_score_a.reload
|
||||
|
|
@ -40,9 +40,9 @@ RSpec.describe GroupScore, type: :model do
|
|||
end
|
||||
|
||||
it 'keeps the teams tied when their direct comparison was a draw' do
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
group_score_a.reload
|
||||
|
|
@ -59,10 +59,10 @@ RSpec.describe GroupScore, type: :model do
|
|||
let(:team_b) { create(:team) }
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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_b, points: 1)
|
||||
end
|
||||
|
|
@ -95,7 +95,7 @@ RSpec.describe GroupScore, type: :model do
|
|||
|
||||
it 'reports an unresolved tie when head-to-head was a draw' do
|
||||
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_b, points: 2)
|
||||
end
|
||||
|
|
@ -112,11 +112,11 @@ RSpec.describe GroupScore, type: :model do
|
|||
|
||||
it 'falls back to hidden points when direct comparison is still tied' do
|
||||
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_b, points: 2)
|
||||
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_b, hidden_points: 1)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ rescue ActiveRecord::PendingMigrationError => e
|
|||
end
|
||||
RSpec.configure do |config|
|
||||
# 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
|
||||
# TODO REVERT ME
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ RSpec.describe TournamentsController, type: :routing do
|
|||
expect(delete: '/tournaments/1').to route_to('tournaments#destroy', id: '1')
|
||||
end
|
||||
|
||||
it 'routes to #sync_state' do
|
||||
expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1')
|
||||
end
|
||||
it 'routes to #sync_state' do
|
||||
expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1')
|
||||
end
|
||||
|
||||
it 'routes to #test_sync' do
|
||||
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
||||
it 'routes to #test_sync' do
|
||||
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RSpec.describe GroupStageService do
|
|||
before do
|
||||
@teams1 = 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
|
||||
describe '#generate_group_stage' do
|
||||
let(:prepared_groups_groupstage) do
|
||||
|
|
@ -52,7 +52,7 @@ RSpec.describe GroupStageService do
|
|||
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
|
||||
it 'returns a group' do
|
||||
|
|
@ -205,13 +205,13 @@ RSpec.describe GroupStageService do
|
|||
team_a = create(:team)
|
||||
team_b = create(:team)
|
||||
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 4)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 4)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
create(:group_score, group: 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_a, 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])
|
||||
end
|
||||
|
|
@ -224,14 +224,14 @@ RSpec.describe GroupStageService do
|
|||
playoff_teams_amount: 2,
|
||||
instant_finalists_amount: 2,
|
||||
intermediate_round_participants_amount: 0,
|
||||
group_stage: group_stage)
|
||||
group_stage:)
|
||||
end
|
||||
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(:team_a) { create(:team, tournament: tournament, name: 'A') }
|
||||
let(:team_b) { create(:team, tournament: tournament, name: 'B') }
|
||||
let(:team_c) { create(:team, tournament: tournament, name: 'C') }
|
||||
let(:team_d) { create(:team, tournament: tournament, name: 'D') }
|
||||
let(:team_a) { create(:team, tournament:, name: 'A') }
|
||||
let(:team_b) { create(:team, tournament:, name: 'B') }
|
||||
let(:team_c) { create(:team, tournament:, name: 'C') }
|
||||
let(:team_d) { create(:team, tournament:, name: 'D') }
|
||||
|
||||
before do
|
||||
group_stage.groups = [group]
|
||||
|
|
@ -240,13 +240,13 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
it 'returns direct-comparison metadata for teams separated by head-to-head' do
|
||||
create(:group_match, group: group, state: :finished).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, points: 4)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 4)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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!)
|
||||
|
||||
|
|
@ -269,17 +269,18 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
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(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: 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_c = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||
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!)
|
||||
|
||||
|
|
@ -301,17 +302,18 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
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(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: 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_c = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||
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!)
|
||||
|
||||
|
|
@ -333,17 +335,18 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
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(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :in_progress).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: 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_c = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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:, team: team_c, 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!)
|
||||
|
||||
|
|
@ -352,12 +355,15 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
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_b = create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 4, received_points: 2, position: 2)
|
||||
score_c = create(:group_score, group: 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: 3, scored_points: 2, received_points: 3)
|
||||
score_a = create(:group_score, group:, team: team_a, group_points: 9, scored_points: 5, received_points: 1,
|
||||
position: 1)
|
||||
score_b = create(:group_score, group:, team: team_b, group_points: 6, scored_points: 4, received_points: 2,
|
||||
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!)
|
||||
|
||||
|
|
@ -367,21 +373,22 @@ RSpec.describe GroupStageService do
|
|||
end
|
||||
|
||||
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(:match_score, match: match, team: team_a, points: 2)
|
||||
create(:match_score, match: match, team: team_b, points: 2)
|
||||
create(:group_match, group:, state: :finished).tap do |match|
|
||||
create(:match_score, match:, team: team_a, points: 2)
|
||||
create(:match_score, match:, team: team_b, points: 2)
|
||||
end
|
||||
create(:group_match, group: group, state: :finished, decider: true).tap do |match|
|
||||
create(:match_score, match: match, team: team_a, hidden_points: 5)
|
||||
create(:match_score, match: match, team: team_b, hidden_points: 3)
|
||||
create(:group_match, group:, state: :finished, decider: true).tap do |match|
|
||||
create(:match_score, match:, team: team_a, hidden_points: 5)
|
||||
create(:match_score, match:, team: team_b, hidden_points: 3)
|
||||
end
|
||||
|
||||
score_a = create(:group_score, group: 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_c = create(:group_score, group: 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_a = create(:group_score, group:, team: team_a, 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:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||
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!)
|
||||
|
||||
|
|
@ -417,7 +424,7 @@ RSpec.describe GroupStageService do
|
|||
|
||||
@tournament = create(:prepared_group_stage_tournament,
|
||||
group_stage: @group_stage,
|
||||
teams: teams,
|
||||
teams:,
|
||||
playoff_teams_amount: 16,
|
||||
instant_finalists_amount: 16,
|
||||
intermediate_round_participants_amount: 0)
|
||||
|
|
@ -427,7 +434,7 @@ RSpec.describe GroupStageService do
|
|||
match.match_scores.each do |ms|
|
||||
# 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.
|
||||
ms.points = 10 - ms.team.name.split(' ').last.to_i
|
||||
ms.points = 10 - ms.team.name.split.last.to_i
|
||||
ms.save!
|
||||
end
|
||||
match.state = 'finished'
|
||||
|
|
@ -450,7 +457,7 @@ RSpec.describe GroupStageService do
|
|||
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 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)
|
||||
end
|
||||
end
|
||||
|
|
@ -469,9 +476,9 @@ RSpec.describe GroupStageService do
|
|||
advancing_teams = GroupStageService.get_advancing_teams(@group_stage)
|
||||
advancing_teams.each_slice(2).to_a.each do |matchup|
|
||||
# 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
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ RSpec.describe MatchService do
|
|||
first_team = match.match_scores.first.team.name
|
||||
second_team = match.match_scores.second.team.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
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ RSpec.describe PlayoffStageService do
|
|||
{ team_size: 9, 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: 24, expected_amount_of_playoff_stages: 5 },
|
||||
{ team_size: 24, expected_amount_of_playoff_stages: 5 }
|
||||
].each do |parameters|
|
||||
it "generates playoff stages for #{parameters[:team_size]} teams" do
|
||||
amount_of_teams = parameters[:team_size]
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ RSpec.describe TeamActionListService do
|
|||
action_name: 'Break',
|
||||
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
|
||||
|
||||
it 'raises when source resolves to no teams' do
|
||||
|
|
@ -62,7 +63,8 @@ RSpec.describe TeamActionListService do
|
|||
action_name: 'Nothing',
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Tournament live payloads' do
|
|||
expect(serialized_match[:position]).to eq(match.position)
|
||||
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[: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))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
|
||||
snapshot = TournamentSnapshotBuilder.build(source)
|
||||
|
||||
described_class.import!(tournament: follower, snapshot: snapshot)
|
||||
described_class.import!(tournament: follower, snapshot:)
|
||||
|
||||
follower.reload
|
||||
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.stages.count).to eq(source.stages.count)
|
||||
expect(follower.matches.count).to eq(source.matches.count)
|
||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
|
||||
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) })
|
||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map do |team|
|
||||
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
|
||||
|
||||
it 'ignores stale snapshots that arrive after a newer one' do
|
||||
|
|
@ -54,7 +58,7 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
|
||||
snapshot = TournamentSnapshotBuilder.build(source)
|
||||
|
||||
described_class.import!(tournament: follower, snapshot: snapshot)
|
||||
described_class.import!(tournament: follower, snapshot:)
|
||||
|
||||
follower.reload
|
||||
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.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.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(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
|
||||
end
|
||||
|
|
@ -95,15 +99,15 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
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 = []
|
||||
|
||||
32.times do |group_index|
|
||||
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(
|
||||
:team,
|
||||
tournament: tournament,
|
||||
tournament:,
|
||||
name: "Leader-G#{group_index + 1}-S#{seed_index + 1}"
|
||||
)
|
||||
end
|
||||
|
|
@ -111,17 +115,15 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero?
|
||||
teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15
|
||||
|
||||
create_group_round_robin!(group: group, teams: teams)
|
||||
create_group_scores!(group: group, teams: teams)
|
||||
create_group_round_robin!(group:, teams:)
|
||||
create_group_scores!(group:, teams:)
|
||||
|
||||
if group_index < 3
|
||||
create_decider_match!(group: group, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index])
|
||||
end
|
||||
create_decider_match!(group:, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) if group_index < 3
|
||||
|
||||
advancing_teams.concat(teams.take(2))
|
||||
end
|
||||
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams)
|
||||
result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: advancing_teams)
|
||||
expect(result).to be_a_success
|
||||
|
||||
tournament.reload
|
||||
|
|
@ -140,23 +142,23 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
]
|
||||
|
||||
pairings.each_with_index do |(left_index, right_index, left_points, right_points), position|
|
||||
match = Match.create!(group: group, state: :finished, position: position, decider: false)
|
||||
MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0)
|
||||
MatchScore.create!(match: match, team: teams[right_index], points: right_points, hidden_points: 0)
|
||||
match = Match.create!(group:, state: :finished, position:, decider: false)
|
||||
MatchScore.create!(match:, team: teams[left_index], points: left_points, hidden_points: 0)
|
||||
MatchScore.create!(match:, team: teams[right_index], points: right_points, hidden_points: 0)
|
||||
end
|
||||
end
|
||||
|
||||
def create_decider_match!(group:, teams:, hidden_points:)
|
||||
match = Match.create!(group: group, state: :finished, position: 6, decider: true)
|
||||
MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first)
|
||||
MatchScore.create!(match: match, team: teams.second, points: 0, hidden_points: hidden_points.last)
|
||||
match = Match.create!(group:, state: :finished, position: 6, decider: true)
|
||||
MatchScore.create!(match:, team: teams.first, points: 0, hidden_points: hidden_points.first)
|
||||
MatchScore.create!(match:, team: teams.second, points: 0, hidden_points: hidden_points.last)
|
||||
end
|
||||
|
||||
def create_group_scores!(group:, teams:)
|
||||
teams.each_with_index do |team, index|
|
||||
GroupScore.create!(
|
||||
group: group,
|
||||
team: team,
|
||||
group:,
|
||||
team:,
|
||||
group_points: 9 - index,
|
||||
scored_points: 12 - index,
|
||||
received_points: index,
|
||||
|
|
@ -185,7 +187,7 @@ RSpec.describe TournamentSnapshotImporter do
|
|||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||
timestamp: tournament.timestamp&.iso8601,
|
||||
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|
|
||||
{
|
||||
key: list.key,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ RSpec.describe TournamentSyncEnqueue do
|
|||
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
||||
sync_auth_token: 'shared-secret')
|
||||
entry = TournamentSyncQueueEntry.create!(
|
||||
tournament: tournament,
|
||||
tournament:,
|
||||
snapshot_json: '{"old":true}',
|
||||
status: 'pending',
|
||||
attempts: 4,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ RSpec.describe TournamentSyncProcessor do
|
|||
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
||||
sync_auth_token: 'shared-secret')
|
||||
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!
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module PerformanceHelpers
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def create_group_stage_tournament(groups_count:, teams_per_group:)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'ruby-prof'
|
||||
|
||||
RSpec.configure do |config|
|
||||
|
|
@ -10,4 +12,4 @@ RSpec.configure do |config|
|
|||
printer = RubyProf::FlatPrinter.new(result)
|
||||
printer.print(File.open("profile_#{example.full_description.parameterize}.txt", 'w+'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue