fix: clear RuboCop offenses
This commit is contained in:
parent
4bd1531408
commit
cfb07e82fc
|
|
@ -13,6 +13,21 @@ include:
|
||||||
- project: 'turniere/turniere-infra'
|
- project: 'turniere/turniere-infra'
|
||||||
file: '/ci/pipeline.yaml'
|
file: '/ci/pipeline.yaml'
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
needs:
|
||||||
|
- job: build_image
|
||||||
|
image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE != "push"
|
||||||
|
when: never
|
||||||
|
- when: always
|
||||||
|
script:
|
||||||
|
- cd /app
|
||||||
|
- inv lint
|
||||||
|
|
||||||
rails spec:
|
rails spec:
|
||||||
stage: test
|
stage: test
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
38
.rubocop.yml
38
.rubocop.yml
|
|
@ -4,6 +4,8 @@ require:
|
||||||
|
|
||||||
# Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56
|
# Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56
|
||||||
AllCops:
|
AllCops:
|
||||||
|
NewCops: disable
|
||||||
|
SuggestExtensions: false
|
||||||
Exclude:
|
Exclude:
|
||||||
- "bin/**/*"
|
- "bin/**/*"
|
||||||
- "db/schema.rb"
|
- "db/schema.rb"
|
||||||
|
|
@ -13,15 +15,15 @@ Rails:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
# Commonly used screens these days easily fit more than 80 characters.
|
# Commonly used screens these days easily fit more than 80 characters.
|
||||||
Metrics/LineLength:
|
Layout/LineLength:
|
||||||
Max: 120
|
Max: 140
|
||||||
|
|
||||||
# Too short methods lead to extraction of single-use methods, which can make
|
# Too short methods lead to extraction of single-use methods, which can make
|
||||||
# the code easier to read (by naming things), but can also clutter the class
|
# the code easier to read (by naming things), but can also clutter the class
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- "db/migrate/*"
|
- "db/migrate/*"
|
||||||
Max: 50
|
Max: 60
|
||||||
|
|
||||||
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
|
|
@ -33,16 +35,42 @@ Metrics/ModuleLength:
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Exclude:
|
Exclude:
|
||||||
- "db/migrate/*"
|
- "db/migrate/*"
|
||||||
Max: 50
|
Max: 75
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
ExcludedMethods:
|
Max: 30
|
||||||
|
AllowedMethods:
|
||||||
- "namespace"
|
- "namespace"
|
||||||
- "create_table"
|
- "create_table"
|
||||||
Exclude:
|
Exclude:
|
||||||
- "config/**/*.rb"
|
- "config/**/*.rb"
|
||||||
- "spec/**/*.rb"
|
- "spec/**/*.rb"
|
||||||
|
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 22
|
||||||
|
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Max: 7
|
||||||
|
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 24
|
||||||
|
|
||||||
|
Rails/ReversibleMigration:
|
||||||
|
Exclude:
|
||||||
|
- "db/migrate/**/*"
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Exclude:
|
||||||
|
- "app/models/tournament_sync_queue_entry.rb"
|
||||||
|
- "app/services/tournament_sync_pusher.rb"
|
||||||
|
- "db/migrate/**/*"
|
||||||
|
- "spec/controllers/tournaments_controller_spec.rb"
|
||||||
|
|
||||||
|
Style/MultilineBlockChain:
|
||||||
|
Exclude:
|
||||||
|
- "e2e/lib/api_client.rb"
|
||||||
|
- "spec/e2e/http/tournament_follow_sync_spec.rb"
|
||||||
|
|
||||||
# Disable documentation checks for now
|
# Disable documentation checks for now
|
||||||
# should be done with yard instead
|
# should be done with yard instead
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
|
|
|
||||||
4
Gemfile
4
Gemfile
|
|
@ -44,8 +44,8 @@ group :test, optional: true do
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
gem 'shoulda-matchers'
|
|
||||||
gem 'ruby-prof'
|
gem 'ruby-prof'
|
||||||
|
gem 'shoulda-matchers'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, optional: true do
|
group :development, optional: true do
|
||||||
|
|
@ -53,13 +53,13 @@ group :development, optional: true do
|
||||||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||||
gem 'railroady'
|
gem 'railroady'
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
gem 'sqlite3'
|
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
gem 'rubocop-performance'
|
gem 'rubocop-performance'
|
||||||
gem 'rubocop-rails'
|
gem 'rubocop-rails'
|
||||||
gem 'solargraph'
|
gem 'solargraph'
|
||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-watcher-listen', '~> 2.0.0'
|
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||||
|
gem 'sqlite3'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,7 @@ class MatchesController < ApplicationController
|
||||||
handle_match_end if new_state == 'finished'
|
handle_match_end if new_state == 'finished'
|
||||||
if @match.group_match? && new_state == 'in_progress'
|
if @match.group_match? && new_state == 'in_progress'
|
||||||
group = @match.group
|
group = @match.group
|
||||||
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||||
logger.warn "Updating groups group score failed for #{group}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @match
|
render json: @match
|
||||||
|
|
@ -66,7 +64,8 @@ class MatchesController < ApplicationController
|
||||||
# PATCH /matches/1/swap
|
# PATCH /matches/1/swap
|
||||||
def swap
|
def swap
|
||||||
unless @match.swappable_with?(@swap_target)
|
unless @match.swappable_with?(@swap_target)
|
||||||
render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_entity
|
render json: { error: 'Matches must be different group matches from the same group' },
|
||||||
|
status: :unprocessable_entity
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -83,7 +82,8 @@ class MatchesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
|
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
|
||||||
render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group]
|
render json: swapped_matches, each_serializer: ExtendedMatchSerializer,
|
||||||
|
include: %w[match_scores.team bets stage group]
|
||||||
push_sync_if_needed!(synced_tournament)
|
push_sync_if_needed!(synced_tournament)
|
||||||
TournamentLiveBroadcast.call(synced_tournament.id)
|
TournamentLiveBroadcast.call(synced_tournament.id)
|
||||||
end
|
end
|
||||||
|
|
@ -93,9 +93,7 @@ class MatchesController < ApplicationController
|
||||||
def handle_match_end
|
def handle_match_end
|
||||||
if @match.group_match?
|
if @match.group_match?
|
||||||
group = @match.group
|
group = @match.group
|
||||||
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||||
logger.warn "Updating groups group score failed for #{group}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return if @match.group_match?
|
return if @match.group_match?
|
||||||
|
|
@ -116,11 +114,9 @@ class MatchesController < ApplicationController
|
||||||
def validate_params
|
def validate_params
|
||||||
case match_params['state']
|
case match_params['state']
|
||||||
when 'in_progress'
|
when 'in_progress'
|
||||||
render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity \
|
render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity unless @match.not_started?
|
||||||
unless @match.not_started?
|
|
||||||
when 'finished'
|
when 'finished'
|
||||||
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity \
|
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity unless @match.in_progress?
|
||||||
unless @match.in_progress?
|
|
||||||
else
|
else
|
||||||
render json: { error: 'Invalid target state' }, status: :unprocessable_entity
|
render json: { error: 'Invalid target state' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
@ -162,11 +158,9 @@ class MatchesController < ApplicationController
|
||||||
state: :not_started,
|
state: :not_started,
|
||||||
position: (@group.matches.maximum(:position) || -1) + 1,
|
position: (@group.matches.maximum(:position) || -1) + 1,
|
||||||
decider: true,
|
decider: true,
|
||||||
match_scores: teams.map { |team| MatchScore.new(team: team) }
|
match_scores: teams.map { |team| MatchScore.new(team:) }
|
||||||
).tap do |match|
|
).tap do |match|
|
||||||
if team_ids.size != 2 || teams.size != 2
|
match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2
|
||||||
match.errors.add(:team_ids, 'must contain exactly two teams from this group')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class StagesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
@stage.groups.each do |group|
|
@stage.groups.each do |group|
|
||||||
next if UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
next if UpdateGroupsGroupScoresAndSave.call(group:).success?
|
||||||
|
|
||||||
render json: { error: 'Updating group scores failed' }, status: :unprocessable_entity
|
render json: { error: 'Updating group scores failed' }, status: :unprocessable_entity
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
|
|
@ -61,7 +61,7 @@ class StagesController < ApplicationController
|
||||||
if blocking_ties.any?
|
if blocking_ties.any?
|
||||||
render json: {
|
render json: {
|
||||||
error: 'Playoffs cannot be generated yet because a decider match is required for an advancing place',
|
error: 'Playoffs cannot be generated yet because a decider match is required for an advancing place',
|
||||||
blocking_ties: blocking_ties
|
blocking_ties:
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,12 @@ class TournamentsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
raise ActionController::ParameterMissing, :teams if teams.nil?
|
raise ActionController::ParameterMissing, :teams if teams.nil?
|
||||||
|
|
||||||
if group_stage
|
if group_stage
|
||||||
params.require(:playoff_teams_amount)
|
params.require(:playoff_teams_amount)
|
||||||
groups = organize_teams_in_groups(teams)
|
groups = organize_teams_in_groups(teams)
|
||||||
# add groups to tournament
|
# add groups to tournament
|
||||||
result = AddGroupStageToTournamentAndSave.call(tournament: tournament, groups: groups)
|
result = AddGroupStageToTournamentAndSave.call(tournament:, groups:)
|
||||||
# associate provided teams with tournament on success
|
# associate provided teams with tournament on success
|
||||||
tournament.teams = groups.flatten if result.success?
|
tournament.teams = groups.flatten if result.success?
|
||||||
else
|
else
|
||||||
|
|
@ -83,7 +84,7 @@ class TournamentsController < ApplicationController
|
||||||
# associate provided teams with tournament
|
# associate provided teams with tournament
|
||||||
tournament.teams = teams
|
tournament.teams = teams
|
||||||
# add playoff stage to tournament
|
# add playoff stage to tournament
|
||||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: tournament.teams)
|
result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: tournament.teams)
|
||||||
end
|
end
|
||||||
# validate tournament
|
# validate tournament
|
||||||
unless tournament.valid?
|
unless tournament.valid?
|
||||||
|
|
@ -165,7 +166,6 @@ class TournamentsController < ApplicationController
|
||||||
}, status: :bad_gateway
|
}, status: :bad_gateway
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def timer_params
|
def timer_params
|
||||||
|
|
@ -253,7 +253,7 @@ class TournamentsController < ApplicationController
|
||||||
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
||||||
|
|
||||||
teams = params['teams']
|
teams = params['teams']
|
||||||
return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero?
|
return if teams.is_a?(Array) && teams.count { |t| !(t.is_a? ActionController::Parameters) }.zero?
|
||||||
|
|
||||||
render json: { error: 'Invalid teams array' }, status: :unprocessable_entity
|
render json: { error: 'Invalid teams array' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
@ -288,12 +288,16 @@ class TournamentsController < ApplicationController
|
||||||
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_state_params
|
def sync_state_params
|
||||||
params.require(:snapshot).permit!.to_h
|
params.require(:snapshot).permit!.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize_sync_request!
|
def authorize_sync_request!
|
||||||
return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_entity unless @tournament.sync_accepts_push?
|
unless @tournament.sync_accepts_push?
|
||||||
|
return render json: { error: 'Tournament is not configured to accept sync pushes' },
|
||||||
|
status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
|
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
|
||||||
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
||||||
|
|
@ -346,17 +350,19 @@ class TournamentsController < ApplicationController
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if timer_mode.present? && !%w[countdown countup].include?(timer_mode)
|
if timer_mode.present? && %w[countdown countup].exclude?(timer_mode)
|
||||||
return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
# throw error if both timestamp and timestamp_seconds are present
|
# throw error if both timestamp and timestamp_seconds are present
|
||||||
if timestamp.present? && timestamp_seconds.present?
|
if timestamp.present? && timestamp_seconds.present?
|
||||||
return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' }, status: :unprocessable_entity
|
return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if timer_mode == 'countup' && timestamp_seconds.present?
|
if timer_mode == 'countup' && timestamp_seconds.present?
|
||||||
return render json: { error: 'timestamp_seconds is only allowed for countdown mode' }, status: :unprocessable_entity
|
return render json: { error: 'timestamp_seconds is only allowed for countdown mode' },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if timestamp_seconds.present?
|
if timestamp_seconds.present?
|
||||||
|
|
@ -366,7 +372,10 @@ class TournamentsController < ApplicationController
|
||||||
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity if timestamp_seconds <= 0
|
if timestamp_seconds <= 0
|
||||||
|
return render json: { error: 'Timestamp must be in the future for countdown mode' },
|
||||||
|
status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
parsed_time = Time.zone.now + timestamp_seconds
|
parsed_time = Time.zone.now + timestamp_seconds
|
||||||
params[:timestamp] = parsed_time
|
params[:timestamp] = parsed_time
|
||||||
|
|
@ -377,21 +386,22 @@ class TournamentsController < ApplicationController
|
||||||
if parsed_time.nil?
|
if parsed_time.nil?
|
||||||
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||||
elsif timer_mode != 'countup' && !parsed_time.future?
|
elsif timer_mode != 'countup' && !parsed_time.future?
|
||||||
return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity
|
return render json: { error: 'Timestamp must be in the future for countdown mode' },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
params[:timestamp] = parsed_time
|
params[:timestamp] = parsed_time
|
||||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
elsif timer_mode == 'countup'
|
elsif timer_mode == 'countup'
|
||||||
params[:timestamp] = Time.zone.now
|
params[:timestamp] = Time.zone.now
|
||||||
elsif timer_mode.present?
|
elsif timer_mode.present?
|
||||||
return render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
|
render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
else
|
else
|
||||||
return render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
|
render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class AdvanceTeamsInIntermediateStage
|
||||||
# populate stage below with the "winners" from single team matches
|
# populate stage below with the "winners" from single team matches
|
||||||
matches.select { |m| m.state == 'single_team' }
|
matches.select { |m| m.state == 'single_team' }
|
||||||
.each do |match|
|
.each do |match|
|
||||||
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success?
|
context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
|
||||||
end
|
end
|
||||||
(context.object_to_save ||= []) << intermediate_stage
|
(context.object_to_save ||= []) << intermediate_stage
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class Beamer < ApplicationRecord
|
||||||
BeamerBroadcast.call(
|
BeamerBroadcast.call(
|
||||||
tournament_id: snapshot.fetch(:tournament_id),
|
tournament_id: snapshot.fetch(:tournament_id),
|
||||||
beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq,
|
beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq,
|
||||||
payload: BeamerBroadcastPayload.build(snapshot: snapshot, event: broadcast_event)
|
payload: BeamerBroadcastPayload.build(snapshot:, event: broadcast_event)
|
||||||
)
|
)
|
||||||
ensure
|
ensure
|
||||||
@broadcast_previous_name = nil
|
@broadcast_previous_name = nil
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ class Stage < ApplicationRecord
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
if !matches.size.zero?
|
if !matches.empty?
|
||||||
matches.map(&:teams).flatten.uniq
|
matches.map(&:teams).flatten.uniq
|
||||||
elsif !groups.size.zero?
|
elsif !groups.empty?
|
||||||
groups.map(&:teams).flatten.uniq
|
groups.map(&:teams).flatten.uniq
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|
@ -20,9 +20,9 @@ class Stage < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def over?
|
def over?
|
||||||
return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero?
|
return matches.find { |m| m.state != 'finished' }.nil? unless matches.empty?
|
||||||
|
|
||||||
unless groups.size.zero? && groups.map(&:matches).flatten.size.zero?
|
unless groups.empty? && groups.map(&:matches).flatten.empty?
|
||||||
relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
|
relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
|
||||||
return relevant_matches.find { |m| m.state != 'finished' }.nil?
|
return relevant_matches.find { |m| m.state != 'finished' }.nil?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
|
||||||
|
|
||||||
validates :name, :key, :action_name, :source, presence: true
|
validates :name, :key, :action_name, :source, presence: true
|
||||||
validates :key, uniqueness: { scope: :tournament_id }
|
validates :key, uniqueness: { scope: :tournament_id }
|
||||||
validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
|
validates :key,
|
||||||
|
format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
|
||||||
|
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
|
||||||
team_action_items.where.not(team_id: desired_team_ids).destroy_all
|
team_action_items.where.not(team_id: desired_team_ids).destroy_all
|
||||||
|
|
||||||
desired_team_ids.each do |team_id|
|
desired_team_ids.each do |team_id|
|
||||||
team_action_items.find_or_create_by!(team_id: team_id)
|
team_action_items.find_or_create_by!(team_id:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,6 @@ class Tournament < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_configuration_is_complete
|
|
||||||
return if sync_target_url.blank? && sync_auth_token.blank?
|
|
||||||
return if sync_target_url.present? && sync_auth_token.present?
|
|
||||||
return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank?
|
|
||||||
|
|
||||||
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_follower_sync_token_when_disabling_read_only_mode
|
def clear_follower_sync_token_when_disabling_read_only_mode
|
||||||
return if read_only_mode?
|
return if read_only_mode?
|
||||||
return if sync_target_url.present?
|
return if sync_target_url.present?
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord
|
||||||
def acquire_lock!
|
def acquire_lock!
|
||||||
token = SecureRandom.hex(8)
|
token = SecureRandom.hex(8)
|
||||||
updated = self.class
|
updated = self.class
|
||||||
.where(id: id)
|
.where(id:)
|
||||||
.where(locked_at: nil)
|
.where(locked_at: nil)
|
||||||
.or(self.class.where(id: id).where('locked_at < ?', Time.current - LOCK_TTL))
|
.or(self.class.where(id:).where('locked_at < ?', Time.current - LOCK_TTL))
|
||||||
.update_all(locked_at: Time.current, lock_token: token)
|
.update_all(locked_at: Time.current, lock_token: token)
|
||||||
return nil if updated.zero?
|
return nil if updated.zero?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
class BeamerBroadcastPayload
|
class BeamerBroadcastPayload
|
||||||
class << self
|
class << self
|
||||||
def build(beamer: nil, snapshot: nil, event:)
|
def build(event:, beamer: nil, snapshot: nil)
|
||||||
data = snapshot || self.snapshot(beamer)
|
data = snapshot || self.snapshot(beamer)
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "beamer.#{event}",
|
type: "beamer.#{event}",
|
||||||
event: event,
|
event:,
|
||||||
tournament_id: data.fetch(:tournament_id),
|
tournament_id: data.fetch(:tournament_id),
|
||||||
beamer: serialized_beamer(data)
|
beamer: serialized_beamer(data)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@
|
||||||
class GroupStageService
|
class GroupStageService
|
||||||
class << self
|
class << self
|
||||||
def generate_group_stage(groups)
|
def generate_group_stage(groups)
|
||||||
raise 'Cannot generate group stage without groups' if groups.length.zero?
|
raise 'Cannot generate group stage without groups' if groups.empty?
|
||||||
|
|
||||||
# raise an error if the average group size is not a whole number
|
# raise an error if the average group size is not a whole number
|
||||||
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
|
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
|
||||||
|
|
||||||
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
|
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
|
||||||
Stage.new level: -1, groups: groups, state: :in_progress
|
Stage.new level: -1, groups:, state: :in_progress
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_group_object_from(team_array)
|
def get_group_object_from(team_array)
|
||||||
Group.new matches: generate_all_matches_between(team_array),
|
Group.new matches: generate_all_matches_between(team_array),
|
||||||
group_scores: team_array.map { |team| GroupScore.new team: team }
|
group_scores: team_array.map { |team| GroupScore.new team: }
|
||||||
end
|
end
|
||||||
|
|
||||||
def deal_with_spacing_of_teams(matches, team_size)
|
def deal_with_spacing_of_teams(matches, team_size)
|
||||||
|
|
@ -71,7 +71,7 @@ class GroupStageService
|
||||||
def update_group_scores(group)
|
def update_group_scores(group)
|
||||||
changed_group_scores = []
|
changed_group_scores = []
|
||||||
group.teams.each do |team|
|
group.teams.each do |team|
|
||||||
group_score = group.group_scores.find_by(team: team)
|
group_score = group.group_scores.find_by(team:)
|
||||||
matches = group.matches.select { |match| match.teams.include? team }
|
matches = group.matches.select { |match| match.teams.include? team }
|
||||||
# reset previous values
|
# reset previous values
|
||||||
group_score.group_points = 0
|
group_score.group_points = 0
|
||||||
|
|
@ -239,11 +239,9 @@ class GroupStageService
|
||||||
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
||||||
|
|
||||||
end_index = index
|
end_index = index
|
||||||
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||||
end_index += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
{ start_index: start_index, end_index: end_index }
|
{ start_index:, end_index: }
|
||||||
end
|
end
|
||||||
|
|
||||||
def advancing_slots_for_group(group)
|
def advancing_slots_for_group(group)
|
||||||
|
|
@ -272,7 +270,7 @@ class GroupStageService
|
||||||
advancing_slots = advancing_slots_for_group(group)
|
advancing_slots = advancing_slots_for_group(group)
|
||||||
group_ready_for_decider_check = regular_group_matches_finished?(group)
|
group_ready_for_decider_check = regular_group_matches_finished?(group)
|
||||||
|
|
||||||
cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions|
|
cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions|
|
||||||
next if sorted_group_scores.size <= 1
|
next if sorted_group_scores.size <= 1
|
||||||
|
|
||||||
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
||||||
|
|
@ -297,7 +295,8 @@ class GroupStageService
|
||||||
tied: comparison_reason[:tied],
|
tied: comparison_reason[:tied],
|
||||||
needs_decider_match: group_ready_for_decider_check &&
|
needs_decider_match: group_ready_for_decider_check &&
|
||||||
comparison_reason[:tied] &&
|
comparison_reason[:tied] &&
|
||||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
|
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
|
||||||
|
advancing_slots)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class PlayoffStageService
|
||||||
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
||||||
playoffs << initial_stage
|
playoffs << initial_stage
|
||||||
# empty stages are the stages, the tournament is filled with to have the matches ready for later
|
# empty stages are the stages, the tournament is filled with to have the matches ready for later
|
||||||
empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3)
|
empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3)
|
||||||
playoffs.concat empty_stages
|
playoffs.concat empty_stages
|
||||||
playoffs
|
playoffs
|
||||||
end
|
end
|
||||||
|
|
@ -25,7 +25,7 @@ class PlayoffStageService
|
||||||
#
|
#
|
||||||
# @param stage_count [Integer] number of stages to generate
|
# @param stage_count [Integer] number of stages to generate
|
||||||
# @return [Array] the generated stages
|
# @return [Array] the generated stages
|
||||||
def generate_stages_with_empty_matches(stage_count, include_third_place_match = false)
|
def generate_stages_with_empty_matches(stage_count, include_third_place_match: false)
|
||||||
empty_stages = []
|
empty_stages = []
|
||||||
stage_count.times do |i|
|
stage_count.times do |i|
|
||||||
match_count = 2**i
|
match_count = 2**i
|
||||||
|
|
@ -82,7 +82,9 @@ class PlayoffStageService
|
||||||
# of which the winners advance into the match below
|
# of which the winners advance into the match below
|
||||||
# depending on the position of the match, the companion match is either on the left or right of it
|
# depending on the position of the match, the companion match is either on the left or right of it
|
||||||
companion_match = find_companion_match(current_match.position, current_stage)
|
companion_match = find_companion_match(current_match.position, current_stage)
|
||||||
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage)
|
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(
|
||||||
|
current_stage, next_stage
|
||||||
|
)
|
||||||
|
|
||||||
winners = get_winners_of(companion_match, current_match)
|
winners = get_winners_of(companion_match, current_match)
|
||||||
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class TeamActionItemUpdate
|
class TeamActionItemUpdate
|
||||||
class << self
|
class << self
|
||||||
def call(team_action_item:, completed:)
|
def call(team_action_item:, completed:)
|
||||||
new(team_action_item: team_action_item, completed: completed).call
|
new(team_action_item:, completed:).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ class TeamActionItemUpdate
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return false unless team_action_item.update(completed: completed)
|
return false unless team_action_item.update(completed:)
|
||||||
|
|
||||||
TournamentSyncEnqueue.call(team_action_item.tournament)
|
TournamentSyncEnqueue.call(team_action_item.tournament)
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class TournamentSnapshotImporter
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def self.import!(tournament:, snapshot:)
|
def self.import!(tournament:, snapshot:)
|
||||||
new(tournament: tournament, snapshot: snapshot).import!
|
new(tournament:, snapshot:).import!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(tournament:, snapshot:)
|
def initialize(tournament:, snapshot:)
|
||||||
|
|
@ -214,6 +214,7 @@ class TournamentSnapshotImporter
|
||||||
stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
|
stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
|
||||||
stale_records.each(&:destroy!)
|
stale_records.each(&:destroy!)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_time(value)
|
def parse_time(value)
|
||||||
return nil if value.blank?
|
return nil if value.blank?
|
||||||
|
|
||||||
|
|
@ -234,6 +235,6 @@ class TournamentSnapshotImporter
|
||||||
def code_taken_by_another_tournament?(code)
|
def code_taken_by_another_tournament?(code)
|
||||||
return false if code.blank?
|
return false if code.blank?
|
||||||
|
|
||||||
Tournament.where(code: code).where.not(id: tournament.id).exists?
|
Tournament.where(code:).where.not(id: tournament.id).exists?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class TournamentSyncEnqueue
|
||||||
return false unless tournament.sync_push_enabled?
|
return false unless tournament.sync_push_enabled?
|
||||||
|
|
||||||
payload = TournamentSnapshotBuilder.build(fresh_tournament)
|
payload = TournamentSnapshotBuilder.build(fresh_tournament)
|
||||||
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament)
|
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:)
|
||||||
entry.snapshot_json = JSON.dump(payload)
|
entry.snapshot_json = JSON.dump(payload)
|
||||||
entry.status = 'pending'
|
entry.status = 'pending'
|
||||||
entry.attempts = 0
|
entry.attempts = 0
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,9 @@ class TournamentSyncPusher
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_snapshot!(snapshot)
|
def push_snapshot!(snapshot)
|
||||||
response = perform_request(snapshot: snapshot)
|
response = perform_request(snapshot:)
|
||||||
|
|
||||||
unless response.is_a?(Net::HTTPSuccess)
|
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
||||||
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}"
|
|
||||||
end
|
|
||||||
|
|
||||||
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
|
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
|
||||||
true
|
true
|
||||||
|
|
@ -53,7 +51,7 @@ class TournamentSyncPusher
|
||||||
request['accept'] = 'application/json'
|
request['accept'] = 'application/json'
|
||||||
request['content-type'] = 'application/json'
|
request['content-type'] = 'application/json'
|
||||||
request['authorization'] = "Bearer #{tournament.sync_auth_token}"
|
request['authorization'] = "Bearer #{tournament.sync_auth_token}"
|
||||||
request.body = JSON.dump({ snapshot: snapshot })
|
request.body = JSON.dump({ snapshot: })
|
||||||
|
|
||||||
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
||||||
timeout = http_timeout_seconds
|
timeout = http_timeout_seconds
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class UserService
|
||||||
|
|
||||||
def bet!(match, team)
|
def bet!(match, team)
|
||||||
validate_bet! match, team
|
validate_bet! match, team
|
||||||
@user.bets.create! match: match, team: team
|
@user.bets.create! match:, team:
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "bundler/setup" # Set up gems listed in the Gemfile.
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||||
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
|
|
||||||
|
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||||
|
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Rails.application.configure do
|
||||||
|
|
||||||
# Enable/disable caching. By default caching is disabled.
|
# Enable/disable caching. By default caching is disabled.
|
||||||
# Run rails dev:cache to toggle caching.
|
# Run rails dev:cache to toggle caching.
|
||||||
if Rails.root.join('tmp', 'caching-dev.txt').exist?
|
if Rails.root.join('tmp/caching-dev.txt').exist?
|
||||||
config.action_controller.perform_caching = true
|
config.action_controller.perform_caching = true
|
||||||
|
|
||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ Rails.application.configure do
|
||||||
config.active_support.deprecation = :notify
|
config.active_support.deprecation = :notify
|
||||||
|
|
||||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||||
config.log_formatter = ::Logger::Formatter.new
|
config.log_formatter = Logger::Formatter.new
|
||||||
|
|
||||||
# Use a different logger for distributed setups.
|
# Use a different logger for distributed setups.
|
||||||
# require 'syslog/logger'
|
# require 'syslog/logger'
|
||||||
|
|
@ -87,5 +87,5 @@ Rails.application.configure do
|
||||||
}
|
}
|
||||||
|
|
||||||
# Logging to STDOUT (running in docker by default)
|
# Logging to STDOUT (running in docker by default)
|
||||||
config.logger = Logger.new(STDOUT)
|
config.logger = Logger.new($stdout)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
|
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
|
||||||
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
|
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
|
||||||
# notations and behaviors.
|
# notations and behaviors.
|
||||||
Rails.application.config.filter_parameters += [
|
Rails.application.config.filter_parameters += %i[
|
||||||
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
|
passw secret token _key crypt salt certificate otp ssn
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
#
|
#
|
||||||
# This file eases your Rails 7.0 framework defaults upgrade.
|
# This file eases your Rails 7.0 framework defaults upgrade.
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@
|
||||||
].each { |path| Spring.watch(path) }
|
].each { |path| Spring.watch(path) }
|
||||||
|
|
||||||
Spring.after_fork do
|
Spring.after_fork do
|
||||||
if ENV['DEBUGGER_STORED_RUBYLIB']
|
ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path|
|
||||||
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path|
|
next unless path.include?('ruby-debug-ide')
|
||||||
next unless path =~ /ruby-debug-ide/
|
|
||||||
load path + '/ruby-debug-ide/multiprocess/starter.rb'
|
load "#{path}/ruby-debug-ide/multiprocess/starter.rb"
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20190112182829)
|
# This migration comes from active_storage (originally 20190112182829)
|
||||||
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
||||||
def up
|
def up
|
||||||
return unless table_exists?(:active_storage_blobs)
|
return unless table_exists?(:active_storage_blobs)
|
||||||
|
|
||||||
unless column_exists?(:active_storage_blobs, :service_name)
|
return if column_exists?(:active_storage_blobs, :service_name)
|
||||||
|
|
||||||
add_column :active_storage_blobs, :service_name, :string
|
add_column :active_storage_blobs, :service_name, :string
|
||||||
|
|
||||||
if configured_service = ActiveStorage::Blob.service.name
|
if (configured_service = ActiveStorage::Blob.service.name)
|
||||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||||
end
|
end
|
||||||
|
|
||||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
def down
|
||||||
return unless table_exists?(:active_storage_blobs)
|
return unless table_exists?(:active_storage_blobs)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20191206030411)
|
# This migration comes from active_storage (originally 20191206030411)
|
||||||
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||||
def change
|
def change
|
||||||
|
|
@ -8,12 +10,13 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||||
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
||||||
t.string :variation_digest, null: false
|
t.string :variation_digest, null: false
|
||||||
|
|
||||||
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true
|
||||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def primary_key_type
|
def primary_key_type
|
||||||
config = Rails.configuration.generators
|
config = Rails.configuration.generators
|
||||||
config.options[config.orm][:primary_key_type] || :primary_key
|
config.options[config.orm][:primary_key_type] || :primary_key
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20211119233751)
|
# This migration comes from active_storage (originally 20211119233751)
|
||||||
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
||||||
def change
|
def change
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :group_scores, :position, :integer
|
add_column :group_scores, :position, :integer
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
change_column_default :group_scores, :position, 0
|
change_column_default :group_scores, :position, 0
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :tournaments, :timer_end, :datetime
|
add_column :tournaments, :timer_end, :datetime
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index :team_action_lists, [:tournament_id, :key], unique: true
|
add_index :team_action_lists, %i[tournament_id key], unique: true
|
||||||
|
|
||||||
create_table :team_action_items do |t|
|
create_table :team_action_items do |t|
|
||||||
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
|
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
|
||||||
|
|
@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index :team_action_items, [:team_action_list_id, :team_id], unique: true
|
add_index :team_action_items, %i[team_action_list_id team_id], unique: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :tournaments, :timer_mode, :string
|
add_column :tournaments, :timer_mode, :string
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ module TurniereE2E
|
||||||
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
|
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
|
||||||
|
|
||||||
def initialize(base_url:)
|
def initialize(base_url:)
|
||||||
@base_url = base_url.sub(%r{/\z}, '')
|
@base_url = base_url.delete_suffix('/')
|
||||||
@messages = Queue.new
|
@messages = Queue.new
|
||||||
@closed = false
|
@closed = false
|
||||||
end
|
end
|
||||||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
||||||
def subscribe_team_action_lists!(tournament_id:)
|
def subscribe_team_action_lists!(tournament_id:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentTeamActionListsChannel',
|
channel: 'TournamentTeamActionListsChannel',
|
||||||
tournament_id: tournament_id
|
tournament_id:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -30,7 +30,7 @@ module TurniereE2E
|
||||||
def subscribe_timer!(tournament_id:)
|
def subscribe_timer!(tournament_id:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentTimersChannel',
|
channel: 'TournamentTimersChannel',
|
||||||
tournament_id: tournament_id
|
tournament_id:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -41,8 +41,8 @@ module TurniereE2E
|
||||||
def subscribe_beamer!(tournament_id:, beamer_name:)
|
def subscribe_beamer!(tournament_id:, beamer_name:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentBeamersChannel',
|
channel: 'TournamentBeamersChannel',
|
||||||
tournament_id: tournament_id,
|
tournament_id:,
|
||||||
beamer_name: beamer_name
|
beamer_name:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -74,31 +74,31 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_payload!(timeout: 20)
|
def wait_for_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
team_action_lists_payload_message?(message)
|
team_action_lists_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_timer_payload!(timeout: 20)
|
def wait_for_timer_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
timer_payload_message?(message)
|
timer_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_beamer_payload!(timeout: 20)
|
def wait_for_beamer_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
beamer_payload_message?(message)
|
beamer_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_tournament_payload!(timeout: 20)
|
def wait_for_tournament_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
tournament_payload_message?(message)
|
tournament_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_matches_payload!(timeout: 20)
|
def wait_for_matches_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
matches_payload_message?(message)
|
matches_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
@ -117,9 +117,7 @@ module TurniereE2E
|
||||||
websocket_url
|
websocket_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def write(data)
|
delegate :write, to: :socket
|
||||||
socket.write(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
@ -131,7 +129,7 @@ module TurniereE2E
|
||||||
driver.text(
|
driver.text(
|
||||||
JSON.dump(
|
JSON.dump(
|
||||||
command: 'subscribe',
|
command: 'subscribe',
|
||||||
identifier: identifier
|
identifier:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -180,8 +178,8 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def pump_until_open!(timeout: 10)
|
def pump_until_open!(timeout: 10)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
while Time.now < deadline
|
while Time.zone.now < deadline
|
||||||
read_from_socket(timeout: 0.5)
|
read_from_socket(timeout: 0.5)
|
||||||
return if driver.ready_state == 1
|
return if driver.ready_state == 1
|
||||||
end
|
end
|
||||||
|
|
@ -190,14 +188,14 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for!(timeout: 20)
|
def wait_for!(timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
until @messages.empty?
|
until @messages.empty?
|
||||||
message = @messages.pop(true)
|
message = @messages.pop(true)
|
||||||
return message if yield(message)
|
return message if yield(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "websocket message not received within #{timeout}s" if Time.now >= deadline
|
raise "websocket message not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
|
|
||||||
read_from_socket(timeout: 0.5)
|
read_from_socket(timeout: 0.5)
|
||||||
rescue ThreadError
|
rescue ThreadError
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
||||||
attr_reader :base_url
|
attr_reader :base_url
|
||||||
|
|
||||||
def initialize(base_url:)
|
def initialize(base_url:)
|
||||||
@base_url = base_url.sub(%r{/\z}, '')
|
@base_url = base_url.delete_suffix('/')
|
||||||
@auth_headers = {}
|
@auth_headers = {}
|
||||||
@login_credentials = nil
|
@login_credentials = nil
|
||||||
end
|
end
|
||||||
|
|
@ -29,10 +29,10 @@ module TurniereE2E
|
||||||
:post,
|
:post,
|
||||||
'/users',
|
'/users',
|
||||||
body: {
|
body: {
|
||||||
email: email,
|
email:,
|
||||||
password: password,
|
password:,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
username: username
|
username:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
store_auth_headers!(response)
|
store_auth_headers!(response)
|
||||||
|
|
@ -40,14 +40,14 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def login!(email:, password:)
|
def login!(email:, password:)
|
||||||
@login_credentials = { email: email, password: password }
|
@login_credentials = { email:, password: }
|
||||||
@auth_headers = {}
|
@auth_headers = {}
|
||||||
response = request(
|
response = request(
|
||||||
:post,
|
:post,
|
||||||
'/users/sign_in',
|
'/users/sign_in',
|
||||||
body: {
|
body: {
|
||||||
email: email,
|
email:,
|
||||||
password: password
|
password:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
store_auth_headers!(response)
|
store_auth_headers!(response)
|
||||||
|
|
@ -59,19 +59,19 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(path, params: nil, headers: {})
|
def get(path, params: nil, headers: {})
|
||||||
request(:get, path, params: params, headers: headers)
|
request(:get, path, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post(path, body: nil, params: nil, headers: {})
|
def post(path, body: nil, params: nil, headers: {})
|
||||||
request(:post, path, body: body, params: params, headers: headers)
|
request(:post, path, body:, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch(path, body: nil, params: nil, headers: {})
|
def patch(path, body: nil, params: nil, headers: {})
|
||||||
request(:patch, path, body: body, params: params, headers: headers)
|
request(:patch, path, body:, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(path, params: nil, headers: {})
|
def delete(path, params: nil, headers: {})
|
||||||
request(:delete, path, params: params, headers: headers)
|
request(:delete, path, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -80,7 +80,7 @@ module TurniereE2E
|
||||||
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
|
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
|
||||||
|
|
||||||
uri = URI.parse("#{base_url}#{path}")
|
uri = URI.parse("#{base_url}#{path}")
|
||||||
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
uri.query = URI.encode_www_form(params) if params.present?
|
||||||
|
|
||||||
request_class = case method
|
request_class = case method
|
||||||
when :get then Net::HTTP::Get
|
when :get then Net::HTTP::Get
|
||||||
|
|
@ -108,7 +108,8 @@ module TurniereE2E
|
||||||
store_auth_headers!(result)
|
store_auth_headers!(result)
|
||||||
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
|
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
|
||||||
login!(**@login_credentials)
|
login!(**@login_credentials)
|
||||||
return request(method, path, body: body, params: params, headers: headers, retry_on_unauthorized: false, refresh_session: false)
|
return request(method, path, body:, params:, headers:, retry_on_unauthorized: false,
|
||||||
|
refresh_session: false)
|
||||||
end
|
end
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ require 'uri'
|
||||||
|
|
||||||
module TurniereE2E
|
module TurniereE2E
|
||||||
class ScenarioRunner
|
class ScenarioRunner
|
||||||
DEFAULT_PASSWORD = 'password123'.freeze
|
DEFAULT_PASSWORD = 'password123'
|
||||||
|
|
||||||
attr_reader :client
|
attr_reader :client
|
||||||
|
|
||||||
def initialize(base_url:, email: nil, password: nil, username: nil)
|
def initialize(base_url:, email: nil, password: nil, username: nil)
|
||||||
@client = ApiClient.new(base_url: base_url)
|
@client = ApiClient.new(base_url:)
|
||||||
@email = email
|
@email = email
|
||||||
@password = password
|
@password = password
|
||||||
@username = username
|
@username = username
|
||||||
|
|
@ -33,19 +33,28 @@ module TurniereE2E
|
||||||
body: { timestamp_seconds: 600 }
|
body: { timestamp_seconds: 600 }
|
||||||
)
|
)
|
||||||
checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) }
|
checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) }
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set
|
if stop_at == :timer_set
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
upcoming_matches = get!(
|
upcoming_matches = get!(
|
||||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
params: { state: 'upcoming' }
|
params: { state: 'upcoming' }
|
||||||
)
|
)
|
||||||
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
|
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches
|
if stop_at == :upcoming_group_matches
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
finish_group_stage_matches!(tournament)
|
finish_group_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:groups_finished] = summarize_tournament(tournament)
|
checkpoints[:groups_finished] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished
|
if stop_at == :groups_finished
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
|
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||||
checkpoints[:statistics] = {
|
checkpoints[:statistics] = {
|
||||||
|
|
@ -57,21 +66,27 @@ module TurniereE2E
|
||||||
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
if stop_at == :playoffs_created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
if stop_at == :before_final_finish_attempt
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
finish_attempt = finish_final_stage_matches!(tournament)
|
finish_attempt = finish_final_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:final_finish_attempted] = {
|
checkpoints[:final_finish_attempted] = {
|
||||||
finish_attempt: finish_attempt,
|
finish_attempt:,
|
||||||
tournament: summarize_tournament(tournament)
|
tournament: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
|
|
||||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
result(session:, tournament:, checkpoints:, stop_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_group_stage_intermediate_round(stop_at: :final_finish_attempted)
|
def run_group_stage_intermediate_round(stop_at: :final_finish_attempted)
|
||||||
|
|
@ -83,42 +98,54 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
finish_group_stage_matches!(tournament)
|
finish_group_stage_matches!(tournament)
|
||||||
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
if stop_at == :playoffs_created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
if stop_at == :before_final_finish_attempt
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
finish_attempt = finish_final_stage_matches!(tournament)
|
finish_attempt = finish_final_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:final_finish_attempted] = {
|
checkpoints[:final_finish_attempted] = {
|
||||||
finish_attempt: finish_attempt,
|
finish_attempt:,
|
||||||
tournament: summarize_tournament(tournament)
|
tournament: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
|
|
||||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
result(session:, tournament:, checkpoints:, stop_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_playoff_only(team_count:, stop_at: :completed)
|
def run_playoff_only(team_count:, stop_at: :completed)
|
||||||
session = register_session!
|
session = register_session!
|
||||||
tournament = create_tournament!(create_playoff_only_payload(team_count: team_count))
|
tournament = create_tournament!(create_playoff_only_payload(team_count:))
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
run_playoffs_until_complete!(tournament.fetch(:id))
|
run_playoffs_until_complete!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:completed] = summarize_tournament(tournament)
|
checkpoints[:completed] = summarize_tournament(tournament)
|
||||||
|
|
||||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
result(session:, tournament:, checkpoints:, stop_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores)
|
def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores)
|
||||||
|
|
@ -130,7 +157,10 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
group = group_stage_of!(tournament).fetch(:groups).first
|
group = group_stage_of!(tournament).fetch(:groups).first
|
||||||
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
|
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
|
||||||
|
|
@ -142,32 +172,35 @@ module TurniereE2E
|
||||||
}
|
}
|
||||||
|
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
result(session:, tournament:, checkpoints:, stop_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show)
|
def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show)
|
||||||
session = register_session!
|
session = register_session!
|
||||||
tournament = create_tournament!(
|
tournament = create_tournament!(
|
||||||
create_group_stage_payload(
|
create_group_stage_payload(
|
||||||
group_count: group_count,
|
group_count:,
|
||||||
teams_per_group: teams_per_group,
|
teams_per_group:,
|
||||||
playoff_teams_amount: playoff_teams_amount
|
playoff_teams_amount:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session:, tournament:, checkpoints:,
|
||||||
|
stop_at:)
|
||||||
|
end
|
||||||
|
|
||||||
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
||||||
checkpoints[:profiled_show] = profiled_show
|
checkpoints[:profiled_show] = profiled_show
|
||||||
|
|
||||||
result(session: session, tournament: profiled_show.fetch(:tournament), checkpoints: checkpoints, stop_at: stop_at)
|
result(session:, tournament: profiled_show.fetch(:tournament), checkpoints:, stop_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_healthcheck!(timeout_seconds: 30)
|
def wait_for_healthcheck!(timeout_seconds: 30)
|
||||||
started_at = Time.now
|
started_at = Time.zone.now
|
||||||
loop do
|
loop do
|
||||||
begin
|
begin
|
||||||
response = client.get('/healthz')
|
response = client.get('/healthz')
|
||||||
|
|
@ -176,7 +209,7 @@ module TurniereE2E
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds
|
raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.zone.now - started_at > timeout_seconds
|
||||||
|
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
end
|
end
|
||||||
|
|
@ -190,11 +223,11 @@ module TurniereE2E
|
||||||
unique = SecureRandom.hex(6)
|
unique = SecureRandom.hex(6)
|
||||||
email = "e2e-#{unique}@example.com"
|
email = "e2e-#{unique}@example.com"
|
||||||
username = "e2e-#{unique}"
|
username = "e2e-#{unique}"
|
||||||
response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
client.register!(email:, password: DEFAULT_PASSWORD, username:)
|
||||||
return { email: email, username: username } if client.authenticated?
|
return { email:, username: } if client.authenticated?
|
||||||
|
|
||||||
login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
|
login_response = client.login!(email:, password: DEFAULT_PASSWORD)
|
||||||
return { email: email, username: username } if login_response.fetch(:status) == 200 && client.authenticated?
|
return { email:, username: } if login_response.fetch(:status) == 200 && client.authenticated?
|
||||||
|
|
||||||
raise ApiError.new(
|
raise ApiError.new(
|
||||||
'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD',
|
'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD',
|
||||||
|
|
@ -216,19 +249,19 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def get!(path, params: nil)
|
def get!(path, params: nil)
|
||||||
response = get_response!(path, params: params)
|
response = get_response!(path, params:)
|
||||||
expect_status!(response, 200, "get #{path}")
|
expect_status!(response, 200, "get #{path}")
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_response!(path, params: nil)
|
def get_response!(path, params: nil)
|
||||||
response = client.get(path, params: params)
|
response = client.get(path, params:)
|
||||||
expect_status!(response, 200, "get #{path}")
|
expect_status!(response, 200, "get #{path}")
|
||||||
response
|
response
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch!(path, body:)
|
def patch!(path, body:)
|
||||||
response = client.patch(path, body: body)
|
response = client.patch(path, body:)
|
||||||
expect_status!(response, 200, "patch #{path}")
|
expect_status!(response, 200, "patch #{path}")
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
@ -244,9 +277,12 @@ module TurniereE2E
|
||||||
def run_playoffs_until_before_final!(tournament_id)
|
def run_playoffs_until_before_final!(tournament_id)
|
||||||
loop do
|
loop do
|
||||||
tournament = fetch_tournament!(tournament_id)
|
tournament = fetch_tournament!(tournament_id)
|
||||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 }
|
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
|
||||||
.select { |match| match.fetch(:state) == 'not_started' }
|
.select { |match| match.fetch(:state) == 'not_started' }
|
||||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
.sort_by do |match|
|
||||||
|
[-match.fetch(:stage_level),
|
||||||
|
match.fetch(:position)]
|
||||||
|
end
|
||||||
|
|
||||||
break if playable_matches.empty?
|
break if playable_matches.empty?
|
||||||
|
|
||||||
|
|
@ -260,7 +296,10 @@ module TurniereE2E
|
||||||
loop do
|
loop do
|
||||||
tournament = fetch_tournament!(tournament_id)
|
tournament = fetch_tournament!(tournament_id)
|
||||||
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
|
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
|
||||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
.sort_by do |match|
|
||||||
|
[-match.fetch(:stage_level),
|
||||||
|
match.fetch(:position)]
|
||||||
|
end
|
||||||
break if playable_matches.empty?
|
break if playable_matches.empty?
|
||||||
|
|
||||||
playable_matches.each do |match|
|
playable_matches.each do |match|
|
||||||
|
|
@ -276,7 +315,7 @@ module TurniereE2E
|
||||||
refreshed_match = fetch_match!(match.fetch(:id))
|
refreshed_match = fetch_match!(match.fetch(:id))
|
||||||
refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score|
|
refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score|
|
||||||
points = score.dig(:team, :name) == winner_name ? 10 : 1
|
points = score.dig(:team, :name) == winner_name ? 10 : 1
|
||||||
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points })
|
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: })
|
||||||
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
|
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -284,7 +323,9 @@ module TurniereE2E
|
||||||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||||
if expected_finish_error
|
if expected_finish_error
|
||||||
actual_error = finished.fetch(:json).fetch(:error)
|
actual_error = finished.fetch(:json).fetch(:error)
|
||||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
unless actual_error == expected_finish_error
|
||||||
|
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -304,7 +345,7 @@ module TurniereE2E
|
||||||
points = points_by_team_name.fetch(team_name) do
|
points = points_by_team_name.fetch(team_name) do
|
||||||
raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}"
|
raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}"
|
||||||
end
|
end
|
||||||
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points })
|
updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: })
|
||||||
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
|
expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -312,7 +353,9 @@ module TurniereE2E
|
||||||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||||
if expected_finish_error
|
if expected_finish_error
|
||||||
actual_error = finished.fetch(:json).fetch(:error)
|
actual_error = finished.fetch(:json).fetch(:error)
|
||||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
unless actual_error == expected_finish_error
|
||||||
|
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -339,7 +382,9 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def final_stage_matches_of!(tournament)
|
def final_stage_matches_of!(tournament)
|
||||||
final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found')
|
final_stage = tournament.fetch(:stages).find do |stage|
|
||||||
|
stage.fetch(:level).zero?
|
||||||
|
end || raise('final stage not found')
|
||||||
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
|
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -354,7 +399,9 @@ module TurniereE2E
|
||||||
|
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) }
|
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match|
|
||||||
|
match.fetch(:id)
|
||||||
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -384,7 +431,7 @@ module TurniereE2E
|
||||||
match = matches_by_teams.fetch(team_names.sort) do
|
match = matches_by_teams.fetch(team_names.sort) do
|
||||||
raise "match for #{team_names.inspect} not found"
|
raise "match for #{team_names.inspect} not found"
|
||||||
end
|
end
|
||||||
play_match_with_points!(match, points_by_team_name: points_by_team_name)
|
play_match_with_points!(match, points_by_team_name:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -404,7 +451,7 @@ module TurniereE2E
|
||||||
|
|
||||||
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
||||||
teams = group_count.times.flat_map do |group_index|
|
teams = group_count.times.flat_map do |group_index|
|
||||||
teams_per_group.times.map do |seed_index|
|
Array.new(teams_per_group) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -417,8 +464,8 @@ module TurniereE2E
|
||||||
description: 'HTTP lifecycle E2E tournament',
|
description: 'HTTP lifecycle E2E tournament',
|
||||||
public: false,
|
public: false,
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: playoff_teams_amount,
|
playoff_teams_amount:,
|
||||||
teams: teams
|
teams:
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -427,7 +474,7 @@ module TurniereE2E
|
||||||
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
||||||
description: 'HTTP lifecycle E2E tournament',
|
description: 'HTTP lifecycle E2E tournament',
|
||||||
public: false,
|
public: false,
|
||||||
teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } }
|
teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -527,7 +574,7 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_server_timing(header)
|
def parse_server_timing(header)
|
||||||
return {} if header.nil? || header.empty?
|
return {} if header.blank?
|
||||||
|
|
||||||
header.split(',').each_with_object({}) do |entry, sections|
|
header.split(',').each_with_object({}) do |entry, sections|
|
||||||
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
||||||
|
|
@ -539,10 +586,10 @@ module TurniereE2E
|
||||||
|
|
||||||
def result(session:, tournament:, checkpoints:, stop_at:)
|
def result(session:, tournament:, checkpoints:, stop_at:)
|
||||||
{
|
{
|
||||||
session: session,
|
session:,
|
||||||
stop_at: stop_at.to_s,
|
stop_at: stop_at.to_s,
|
||||||
tournament: summarize_tournament(tournament),
|
tournament: summarize_tournament(tournament),
|
||||||
checkpoints: checkpoints
|
checkpoints:
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -557,10 +604,14 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
def login_with_existing_credentials!
|
|
||||||
|
def login_with_existing_credentials!
|
||||||
login_response = client.login!(email: @email, password: @password)
|
login_response = client.login!(email: @email, password: @password)
|
||||||
expect_status!(login_response, 200, "login #{@email}")
|
expect_status!(login_response, 200, "login #{@email}")
|
||||||
raise ApiError.new('login succeeded but no auth headers were returned', status: 200, body: login_response.fetch(:json)) unless client.authenticated?
|
unless client.authenticated?
|
||||||
|
raise ApiError.new('login succeeded but no auth headers were returned', status: 200,
|
||||||
|
body: login_response.fetch(:json))
|
||||||
|
end
|
||||||
|
|
||||||
{ email: @email, username: @username || @email }
|
{ email: @email, username: @username || @email }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ module Local
|
||||||
result = yield
|
result = yield
|
||||||
end
|
end
|
||||||
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
||||||
@measurements << { name: name, duration: duration.round(1), query_count: query_count }
|
@measurements << { name:, duration: duration.round(1), query_count: }
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ options = {
|
||||||
group_count: 4,
|
group_count: 4,
|
||||||
teams_per_group: 4,
|
teams_per_group: 4,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
email: ENV['TURNIERE_E2E_EMAIL'],
|
email: ENV.fetch('TURNIERE_E2E_EMAIL', nil),
|
||||||
password: ENV['TURNIERE_E2E_PASSWORD'],
|
password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil),
|
||||||
username: ENV['TURNIERE_E2E_USERNAME']
|
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionParser.new do |parser|
|
OptionParser.new do |parser|
|
||||||
|
|
@ -22,9 +22,15 @@ OptionParser.new do |parser|
|
||||||
|
|
||||||
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
|
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
|
||||||
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
|
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
|
||||||
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value }
|
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value|
|
||||||
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value }
|
options[:team_count] = value
|
||||||
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value }
|
end
|
||||||
|
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') do |value|
|
||||||
|
options[:group_count] = value
|
||||||
|
end
|
||||||
|
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') do |value|
|
||||||
|
options[:teams_per_group] = value
|
||||||
|
end
|
||||||
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
|
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
|
||||||
options[:playoff_teams_amount] = value
|
options[:playoff_teams_amount] = value
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,9 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
before(:each) { apply_authentication_headers_for @owner }
|
before(:each) { apply_authentication_headers_for @owner }
|
||||||
|
|
||||||
it 'creates a new beamer' do
|
it 'creates a new beamer' do
|
||||||
expect {
|
expect do
|
||||||
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
}.to change(@tournament.beamers, :count).by(1)
|
end.to change(@tournament.beamers, :count).by(1)
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -186,11 +186,11 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
before(:each) { apply_authentication_headers_for @owner }
|
before(:each) { apply_authentication_headers_for @owner }
|
||||||
|
|
||||||
it 'deletes the beamer' do
|
it 'deletes the beamer' do
|
||||||
expect {
|
expect do
|
||||||
delete :destroy, params: {
|
delete :destroy, params: {
|
||||||
tournament_id: @tournament.to_param, id: @beamer.to_param
|
tournament_id: @tournament.to_param, id: @beamer.to_param
|
||||||
}
|
}
|
||||||
}.to change(@tournament.beamers, :count).by(-1)
|
end.to change(@tournament.beamers, :count).by(-1)
|
||||||
expect(response).to have_http_status(:no_content)
|
expect(response).to have_http_status(:no_content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,18 @@ RSpec.describe GroupsController, type: :controller do
|
||||||
|
|
||||||
it 'returns ranking_decision metadata for group scores' do
|
it 'returns ranking_decision metadata for group scores' do
|
||||||
tournament = @group.stage.tournament
|
tournament = @group.stage.tournament
|
||||||
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
@group.stage.update!(level: -1)
|
@group.stage.update!(level: -1)
|
||||||
@group.matches.destroy_all
|
@group.matches.destroy_all
|
||||||
@group.group_scores.destroy_all
|
@group.group_scores.destroy_all
|
||||||
|
|
||||||
team_a = create(:team, tournament: tournament, name: 'A')
|
team_a = create(:team, tournament:, name: 'A')
|
||||||
team_b = create(:team, tournament: tournament, name: 'B')
|
team_b = create(:team, tournament:, name: 'B')
|
||||||
|
|
||||||
create(:group_match, group: @group, state: :finished).tap do |match|
|
create(:group_match, group: @group, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 3)
|
create(:match_score, match:, team: team_a, points: 3)
|
||||||
create(:match_score, match: match, team: team_b, points: 1)
|
create(:match_score, match:, team: team_b, points: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1)
|
score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
expect(match[:teams]).to all(include(:id, :name))
|
expect(match[:teams]).to all(include(:id, :name))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# TODO add test for upcoming once there is test data for a "valid" group stage
|
# TODO: add test for upcoming once there is test data for a "valid" group stage
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
|
|
@ -65,12 +65,14 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
get :show, params: { id: @match.to_param }
|
get :show, params: { id: @match.to_param }
|
||||||
body = deserialize_response response
|
body = deserialize_response response
|
||||||
expect(body[:state]).to eq(@match.state)
|
expect(body[:state]).to eq(@match.state)
|
||||||
expect(body[:match_scores].map { |ms| ms[:id] }).to eq(@match.match_scores.map(&:id))
|
expect(body[:match_scores].pluck(:id)).to eq(@match.match_scores.map(&:id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST #create' do
|
describe 'POST #create' do
|
||||||
let(:group_stage_tournament) { create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match) }
|
let(:group_stage_tournament) do
|
||||||
|
create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match)
|
||||||
|
end
|
||||||
let(:group) { group_stage_tournament.stages.find_by(level: -1).groups.first }
|
let(:group) { group_stage_tournament.stages.find_by(level: -1).groups.first }
|
||||||
let(:team_ids) { group.teams.first(2).map(&:id) }
|
let(:team_ids) { group.teams.first(2).map(&:id) }
|
||||||
|
|
||||||
|
|
@ -81,7 +83,7 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
|
|
||||||
it 'creates a decider match at the end of the group' do
|
it 'creates a decider match at the end of the group' do
|
||||||
expect do
|
expect do
|
||||||
post :create, params: { group_id: group.to_param, team_ids: team_ids }
|
post :create, params: { group_id: group.to_param, team_ids: }
|
||||||
end.to change { group.matches.reload.count }.by(1)
|
end.to change { group.matches.reload.count }.by(1)
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
@ -137,8 +139,8 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
expect(last_match.reload.position).to eq(original_first_position)
|
expect(last_match.reload.position).to eq(original_first_position)
|
||||||
|
|
||||||
body = deserialize_response(response)
|
body = deserialize_response(response)
|
||||||
expect(body.map { |match| match[:id] }).to match_array([first_match.id, last_match.id])
|
expect(body.pluck(:id)).to match_array([first_match.id, last_match.id])
|
||||||
expect(body.map { |match| match[:position] }).to match_array([original_first_position, original_last_position])
|
expect(body.pluck(:position)).to match_array([original_first_position, original_last_position])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects matches from different groups' do
|
it 'rejects matches from different groups' do
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ RSpec.describe StagesController, type: :controller do
|
||||||
match.save!
|
match.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
UpdateGroupsGroupScoresAndSave.call(group: group)
|
UpdateGroupsGroupScoresAndSave.call(group:)
|
||||||
end
|
end
|
||||||
|
|
||||||
stage.reload
|
stage.reload
|
||||||
|
|
@ -179,17 +179,20 @@ RSpec.describe StagesController, type: :controller do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
tournament = group_stage.tournament
|
tournament = group_stage.tournament
|
||||||
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
group = create(:group, stage: group_stage, match_count: 0, number: 1)
|
group = create(:group, stage: group_stage, match_count: 0, number: 1)
|
||||||
group_stage.groups = [group]
|
group_stage.groups = [group]
|
||||||
team_a = create(:team, tournament: tournament, name: 'A')
|
team_a = create(:team, tournament:, name: 'A')
|
||||||
team_b = create(:team, tournament: tournament, name: 'B')
|
team_b = create(:team, tournament:, name: 'B')
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4,
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
received_points: 1)
|
||||||
|
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4,
|
||||||
|
received_points: 1)
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
||||||
|
|
||||||
apply_authentication_headers_for group_stage.owner
|
apply_authentication_headers_for group_stage.owner
|
||||||
|
|
@ -215,17 +218,20 @@ RSpec.describe StagesController, type: :controller do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
tournament = group_stage.tournament
|
tournament = group_stage.tournament
|
||||||
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
group = create(:group, stage: group_stage, match_count: 0, number: 1)
|
group = create(:group, stage: group_stage, match_count: 0, number: 1)
|
||||||
group_stage.groups = [group]
|
group_stage.groups = [group]
|
||||||
team_a = create(:team, tournament: tournament, name: 'A')
|
team_a = create(:team, tournament:, name: 'A')
|
||||||
team_b = create(:team, tournament: tournament, name: 'B')
|
team_b = create(:team, tournament:, name: 'B')
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4,
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
received_points: 1)
|
||||||
|
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4,
|
||||||
|
received_points: 1)
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
||||||
|
|
||||||
apply_authentication_headers_for group_stage.owner
|
apply_authentication_headers_for group_stage.owner
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
get :index
|
get :index
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
public_tournaments = tournaments.select { |t| t[:public] }
|
public_tournaments = tournaments.select { |t| t[:public] }
|
||||||
expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] })
|
expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -35,15 +35,15 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
|
|
||||||
it 'returns all private tournaments' do
|
it 'returns all private tournaments' do
|
||||||
apply_authentication_headers_for @another_user
|
apply_authentication_headers_for @another_user
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
private_tournaments = Tournament.where(owner: @another_user, public: false).map { |t| t[:id] }
|
private_tournaments = Tournament.where(owner: @another_user, public: false).pluck(:id)
|
||||||
returned_private_tournaments = tournaments.filter { |t| !t[:public] }.map { |t| t[:id] }
|
returned_private_tournaments = tournaments.filter { |t| !t[:public] }.pluck(:id)
|
||||||
expect(returned_private_tournaments).to match_array(private_tournaments)
|
expect(returned_private_tournaments).to match_array(private_tournaments)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns no private tournaments for unauthenticated users' do
|
it 'returns no private tournaments for unauthenticated users' do
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
private_tournaments = tournaments.reject { |t| t[:public] }
|
private_tournaments = tournaments.reject { |t| t[:public] }
|
||||||
expect(private_tournaments.size).to eq(0)
|
expect(private_tournaments.size).to eq(0)
|
||||||
|
|
@ -51,14 +51,14 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
|
|
||||||
it 'returns no private tournaments owned by another user' do
|
it 'returns no private tournaments owned by another user' do
|
||||||
apply_authentication_headers_for @user
|
apply_authentication_headers_for @user
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
expect(tournaments.map { |t| t[:id] }).not_to include(@private_tournament.id)
|
expect(tournaments.pluck(:id)).not_to include(@private_tournament.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns no public tournaments' do
|
it 'returns no public tournaments' do
|
||||||
apply_authentication_headers_for @another_user
|
apply_authentication_headers_for @another_user
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
expect(tournaments.count { |t| t[:public] }).to eq(0)
|
expect(tournaments.count { |t| t[:public] }).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
@ -70,15 +70,15 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns all public tournaments' do
|
it 'returns all public tournaments' do
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
public_tournaments = tournaments.select { |t| t[:public] }
|
public_tournaments = tournaments.select { |t| t[:public] }
|
||||||
expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] })
|
expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns no private tournaments' do
|
it 'returns no private tournaments' do
|
||||||
apply_authentication_headers_for @another_user
|
apply_authentication_headers_for @another_user
|
||||||
get :index, params: params
|
get(:index, params:)
|
||||||
tournaments = deserialize_response response
|
tournaments = deserialize_response response
|
||||||
expect(tournaments.count { |t| !t[:public] }).to eq(0)
|
expect(tournaments.count { |t| !t[:public] }).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
@ -224,7 +224,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
describe 'GET #timer' do
|
describe 'GET #timer' do
|
||||||
it 'returns timer state in ISO 8601 format' do
|
it 'returns timer state in ISO 8601 format' do
|
||||||
timestamp = Time.utc(2026, 4, 13, 18, 0, 0)
|
timestamp = Time.utc(2026, 4, 13, 18, 0, 0)
|
||||||
@tournament.update!(timestamp: timestamp, timer_mode: 'countdown')
|
@tournament.update!(timestamp:, timer_mode: 'countdown')
|
||||||
|
|
||||||
get :timer, params: { id: @tournament.to_param }
|
get :timer, params: { id: @tournament.to_param }
|
||||||
|
|
||||||
|
|
@ -242,7 +242,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
it 'stores timezone-aware timestamp values without shifting them' do
|
it 'stores timezone-aware timestamp values without shifting them' do
|
||||||
timestamp = '2030-04-13T18:00:00+02:00'
|
timestamp = '2030-04-13T18:00:00+02:00'
|
||||||
|
|
||||||
patch :set_timer, params: { id: @tournament.to_param, timestamp: timestamp }
|
patch :set_timer, params: { id: @tournament.to_param, timestamp: }
|
||||||
|
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
expect(@tournament.reload.timestamp.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601)
|
expect(@tournament.reload.timestamp.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601)
|
||||||
|
|
@ -257,7 +257,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
|
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
expect(@tournament.reload.timer_mode).to eq('countup')
|
expect(@tournament.reload.timer_mode).to eq('countup')
|
||||||
expect(@tournament.timestamp).to be_between(before_request - 1.second, Time.zone.now + 1.second)
|
expect(@tournament.timestamp).to be_between(before_request - 1.second, 1.second.from_now)
|
||||||
expect(deserialize_response(response)[:timer_mode]).to eq('countup')
|
expect(deserialize_response(response)[:timer_mode]).to eq('countup')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
|
|
||||||
let(:owner_client) { login_client(email: owner_email, password: owner_password) }
|
let(:owner_client) { login_client(email: owner_email, password: owner_password) }
|
||||||
let(:other_client) { secondary_client }
|
let(:other_client) { secondary_client }
|
||||||
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) }
|
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
|
||||||
|
|
||||||
it 'reports health' do
|
it 'reports health' do
|
||||||
response = anonymous_client.get('/healthz')
|
response = anonymous_client.get('/healthz')
|
||||||
|
|
@ -34,14 +34,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
email = "http-e2e-#{unique}@example.com"
|
email = "http-e2e-#{unique}@example.com"
|
||||||
password = 'password123'
|
password = 'password123'
|
||||||
username = "http-e2e-#{unique}"
|
username = "http-e2e-#{unique}"
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
registration = client.post('/users', body: {
|
registration = client.post('/users', body: {
|
||||||
email: email,
|
email:,
|
||||||
password: password,
|
password:,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
username: username
|
username:
|
||||||
})
|
})
|
||||||
login = TurniereE2E::ApiClient.new(base_url: base_url).login!(email: email, password: password)
|
login = TurniereE2E::ApiClient.new(base_url:).login!(email:, password:)
|
||||||
|
|
||||||
expect([500, *200..299]).to include(registration[:status])
|
expect([500, *200..299]).to include(registration[:status])
|
||||||
expect(registration[:json]).not_to eq({})
|
expect(registration[:json]).not_to eq({})
|
||||||
|
|
@ -59,12 +59,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' })
|
invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' })
|
||||||
|
|
||||||
expect(public_index[:status]).to eq(200)
|
expect(public_index[:status]).to eq(200)
|
||||||
expect(public_index[:json].map { |tournament| tournament[:id] }).to include(public_tournament.fetch(:id))
|
expect(public_index[:json].pluck(:id)).to include(public_tournament.fetch(:id))
|
||||||
expect(public_index[:json].map { |tournament| tournament[:id] }).not_to include(private_tournament.fetch(:id))
|
expect(public_index[:json].pluck(:id)).not_to include(private_tournament.fetch(:id))
|
||||||
|
|
||||||
expect(private_index[:status]).to eq(200)
|
expect(private_index[:status]).to eq(200)
|
||||||
expect(private_index[:json].map { |tournament| tournament[:id] }).to include(private_tournament.fetch(:id))
|
expect(private_index[:json].pluck(:id)).to include(private_tournament.fetch(:id))
|
||||||
expect(private_index[:json].map { |tournament| tournament[:id] }).not_to include(public_tournament.fetch(:id))
|
expect(private_index[:json].pluck(:id)).not_to include(public_tournament.fetch(:id))
|
||||||
|
|
||||||
expect(simple_show[:status]).to eq(200)
|
expect(simple_show[:status]).to eq(200)
|
||||||
expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id))
|
expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id))
|
||||||
|
|
@ -93,10 +93,10 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
teams: [{ name: 'Solo Team' }]
|
teams: [{ name: 'Solo Team' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(group_stage.fetch(:stages).map { |stage| stage[:level] }).to include(-1)
|
expect(group_stage.fetch(:stages).pluck(:level)).to include(-1)
|
||||||
expect(group_stage.fetch(:teams).count).to eq(16)
|
expect(group_stage.fetch(:teams).count).to eq(16)
|
||||||
|
|
||||||
expect(playoff_only.fetch(:stages).map { |stage| stage[:level] }.sort).to eq([0, 1])
|
expect(playoff_only.fetch(:stages).pluck(:level).sort).to eq([0, 1])
|
||||||
expect(playoff_only.fetch(:teams).count).to eq(4)
|
expect(playoff_only.fetch(:teams).count).to eq(4)
|
||||||
|
|
||||||
expect(invalid_teams[:status]).to eq(422)
|
expect(invalid_teams[:status]).to eq(422)
|
||||||
|
|
@ -133,7 +133,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
timestamp_seconds: 120,
|
timestamp_seconds: 120,
|
||||||
timestamp: (Time.now.utc + 240).iso8601
|
timestamp: (Time.now.utc + 240).iso8601
|
||||||
})
|
})
|
||||||
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 'nope' })
|
timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer",
|
||||||
|
body: { timestamp_seconds: 'nope' })
|
||||||
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 0 })
|
timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 0 })
|
||||||
timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: {
|
timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: {
|
||||||
timer_mode: 'countup',
|
timer_mode: 'countup',
|
||||||
|
|
@ -201,12 +202,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
|
|
||||||
statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics")
|
statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||||
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches")
|
||||||
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' })
|
matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
params: { state: 'not_started' })
|
||||||
|
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { state: 'upcoming' })
|
||||||
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||||
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
||||||
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||||
unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}",
|
||||||
|
body: { state: 'in_progress' })
|
||||||
stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||||
invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' })
|
invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' })
|
||||||
forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||||
|
|
@ -225,7 +229,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}")
|
score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}")
|
||||||
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 })
|
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 })
|
||||||
forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
|
forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
|
||||||
unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
|
unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}",
|
||||||
|
body: { points: 3 })
|
||||||
bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
|
bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
|
||||||
restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||||
playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics")
|
playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics")
|
||||||
|
|
@ -268,9 +273,9 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(matches_all[:status]).to eq(200)
|
expect(matches_all[:status]).to eq(200)
|
||||||
expect(matches_all[:json].count).to eq(tournament_match_count(tournament))
|
expect(matches_all[:json].count).to eq(tournament_match_count(tournament))
|
||||||
expect(matches_not_started[:status]).to eq(200)
|
expect(matches_not_started[:status]).to eq(200)
|
||||||
expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
expect(matches_not_started[:json].pluck(:id)).to include(match.fetch(:id))
|
||||||
expect(matches_upcoming[:status]).to eq(200)
|
expect(matches_upcoming[:status]).to eq(200)
|
||||||
expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id))
|
expect(matches_upcoming[:json].pluck(:id)).to include(match.fetch(:id))
|
||||||
expect(match_show[:status]).to eq(200)
|
expect(match_show[:status]).to eq(200)
|
||||||
expect(invalid_match_update[:status]).to eq(422)
|
expect(invalid_match_update[:status]).to eq(422)
|
||||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||||
|
|
@ -336,31 +341,38 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
.fetch(:groups)
|
.fetch(:groups)
|
||||||
.find { |item| item.fetch(:id) == group.fetch(:id) }
|
.find { |item| item.fetch(:id) == group.fetch(:id) }
|
||||||
swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
||||||
upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { state: 'upcoming' })
|
||||||
|
|
||||||
finish_group_match_with_winner!(match_id: fourth_match.fetch(:id))
|
finish_group_match_with_winner!(match_id: fourth_match.fetch(:id))
|
||||||
|
|
||||||
upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { state: 'upcoming' })
|
||||||
|
|
||||||
expect(upcoming_before[:status]).to eq(200)
|
expect(upcoming_before[:status]).to eq(200)
|
||||||
expect(upcoming_match_for_group(upcoming_before, group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id))
|
expect(upcoming_match_for_group(upcoming_before,
|
||||||
|
group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id))
|
||||||
|
|
||||||
expect(swap_response[:status]).to eq(200)
|
expect(swap_response[:status]).to eq(200)
|
||||||
expect(swap_response[:json].map { |match| match[:id] }).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)])
|
expect(swap_response[:json].pluck(:id)).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)])
|
||||||
|
|
||||||
expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id))
|
expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id))
|
||||||
expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position))
|
expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position))
|
||||||
expect(swapped_matches.find { |match| match.fetch(:id) == first_match.fetch(:id) }.fetch(:position)).to eq(fourth_match.fetch(:position))
|
expect(swapped_matches.find do |match|
|
||||||
|
match.fetch(:id) == first_match.fetch(:id)
|
||||||
|
end.fetch(:position)).to eq(fourth_match.fetch(:position))
|
||||||
expect(upcoming_after_swap[:status]).to eq(200)
|
expect(upcoming_after_swap[:status]).to eq(200)
|
||||||
expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
|
expect(upcoming_match_for_group(upcoming_after_swap,
|
||||||
|
group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
|
||||||
|
|
||||||
expect(upcoming_after_finish[:status]).to eq(200)
|
expect(upcoming_after_finish[:status]).to eq(200)
|
||||||
expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id))
|
expect(upcoming_match_for_group(upcoming_after_finish,
|
||||||
|
group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client(email:, password:)
|
def login_client(email:, password:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.login!(email: email, password: password)
|
response = client.login!(email:, password:)
|
||||||
|
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
expect(client.authenticated?).to eq(true)
|
expect(client.authenticated?).to eq(true)
|
||||||
|
|
@ -368,15 +380,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def secondary_client
|
def secondary_client
|
||||||
alt_email = ENV['TURNIERE_E2E_ALT_EMAIL']
|
alt_email = ENV.fetch('TURNIERE_E2E_ALT_EMAIL', nil)
|
||||||
alt_password = ENV['TURNIERE_E2E_ALT_PASSWORD']
|
alt_password = ENV.fetch('TURNIERE_E2E_ALT_PASSWORD', nil)
|
||||||
if alt_email && alt_password
|
if alt_email && alt_password
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.login!(email: alt_email, password: alt_password)
|
response = client.login!(email: alt_email, password: alt_password)
|
||||||
return client if response[:status] == 200 && client.authenticated?
|
return client if response[:status] == 200 && client.authenticated?
|
||||||
end
|
end
|
||||||
|
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
unique = unique_suffix
|
unique = unique_suffix
|
||||||
response = client.post('/users', body: {
|
response = client.post('/users', body: {
|
||||||
email: "http-e2e-secondary-#{unique}@example.com",
|
email: "http-e2e-secondary-#{unique}@example.com",
|
||||||
|
|
@ -394,11 +406,11 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
payload = {
|
payload = {
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'HTTP API surface E2E group-stage tournament',
|
description: 'HTTP API surface E2E group-stage tournament',
|
||||||
public: public,
|
public:,
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
teams: 4.times.flat_map do |group_index|
|
teams: 4.times.flat_map do |group_index|
|
||||||
4.times.map do |seed_index|
|
Array.new(4) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -409,22 +421,22 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
|
|
||||||
response = client.post('/tournaments', body: payload)
|
response = client.post('/tournaments', body: payload)
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
fetch_tournament(client:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
def create_playoff_tournament(client:, public:, name_prefix:)
|
||||||
payload = {
|
payload = {
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'HTTP API surface E2E playoff tournament',
|
description: 'HTTP API surface E2E playoff tournament',
|
||||||
public: public,
|
public:,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post('/tournaments', body: payload)
|
response = client.post('/tournaments', body: payload)
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
fetch_tournament(client:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_tournament(client:, tournament_id:)
|
def fetch_tournament(client:, tournament_id:)
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
||||||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
description: 'Websocket beamer tournament',
|
description: 'Websocket beamer tournament',
|
||||||
public: true,
|
public: true,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
@ -89,13 +89,13 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if payload.fetch('type') == event_type &&
|
return payload if payload.fetch('type') == event_type &&
|
||||||
payload.dig('beamer', 'display_state') == display_state
|
payload.dig('beamer', 'display_state') == display_state
|
||||||
|
|
||||||
raise "beamer websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,12 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
|
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
|
||||||
|
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
||||||
cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id), beamer_name: follower_default_beamer.fetch(:name))
|
cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: follower_default_beamer.fetch(:name))
|
||||||
cable_client.wait_for_beamer_payload!
|
cable_client.wait_for_beamer_payload!
|
||||||
|
|
||||||
source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id), beamer_name: DEFAULT_BEAMER_NAME)
|
source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id),
|
||||||
|
beamer_name: DEFAULT_BEAMER_NAME)
|
||||||
update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: {
|
update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: {
|
||||||
display_state: 'groups'
|
display_state: 'groups'
|
||||||
})
|
})
|
||||||
|
|
@ -71,7 +73,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
description: 'Follower beamer websocket source tournament',
|
description: 'Follower beamer websocket source tournament',
|
||||||
public: true,
|
public: true,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
@ -107,25 +109,25 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_default_beamer!(tournament_id:, timeout: 20)
|
def wait_for_default_beamer!(tournament_id:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_name: DEFAULT_BEAMER_NAME)
|
beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_name: DEFAULT_BEAMER_NAME)
|
||||||
return beamer if beamer.fetch(:name) == DEFAULT_BEAMER_NAME
|
return beamer if beamer.fetch(:name) == DEFAULT_BEAMER_NAME
|
||||||
|
|
||||||
raise "follower default beamer not present within #{timeout}s" if Time.now >= deadline
|
raise "follower default beamer not present within #{timeout}s" if Time.zone.now >= deadline
|
||||||
|
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if payload.fetch('type') == event_type &&
|
return payload if payload.fetch('type') == event_type &&
|
||||||
payload.dig('beamer', 'display_state') == display_state
|
payload.dig('beamer', 'display_state') == display_state
|
||||||
|
|
||||||
raise "follower beamer websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "follower beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||||
let(:sync_token) { "sync-#{SecureRandom.hex(8)}" }
|
let(:sync_token) { "sync-#{SecureRandom.hex(8)}" }
|
||||||
let(:source_owner_client) { login_client(base_url: source_base_url, email: owner_email, password: owner_password) }
|
let(:source_owner_client) { login_client(base_url: source_base_url, email: owner_email, password: owner_password) }
|
||||||
let(:follower_owner_client) { login_client(base_url: follower_base_url, email: owner_email, password: owner_password) }
|
let(:follower_owner_client) do
|
||||||
|
login_client(base_url: follower_base_url, email: owner_email, password: owner_password)
|
||||||
|
end
|
||||||
let(:source_anonymous_client) { TurniereE2E::ApiClient.new(base_url: source_base_url) }
|
let(:source_anonymous_client) { TurniereE2E::ApiClient.new(base_url: source_base_url) }
|
||||||
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
||||||
|
|
||||||
|
|
@ -43,23 +45,24 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
sync_auth_token: sync_token
|
sync_auth_token: sync_token
|
||||||
})
|
})
|
||||||
expect(configure_sync[:status]).to eq(200)
|
expect(configure_sync[:status]).to eq(200)
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
|
||||||
|
|
||||||
log_phase!('swap matches on leader and verify follower behavior')
|
log_phase!('swap matches on leader and verify follower behavior')
|
||||||
source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||||
first_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first
|
first_source_group = source_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) }
|
||||||
second_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }[1]
|
second_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }[1]
|
||||||
first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }
|
first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||||
first_position_match = first_group_matches.fetch(0)
|
first_position_match = first_group_matches.fetch(0)
|
||||||
fourth_position_match = first_group_matches.fetch(3)
|
fourth_position_match = first_group_matches.fetch(3)
|
||||||
wrong_group_match = second_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }.first
|
wrong_group_match = second_source_group.fetch(:matches).min_by { |match| match.fetch(:position) }
|
||||||
follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||||
first_follower_group = follower_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first
|
first_follower_group = follower_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) }
|
||||||
follower_first_position_match = first_follower_group.fetch(:matches).find do |match|
|
follower_first_position_match = first_follower_group.fetch(:matches).find do |match|
|
||||||
match_team_names(match) == match_team_names(first_position_match)
|
match_team_names(match) == match_team_names(first_position_match)
|
||||||
end
|
end
|
||||||
|
|
@ -76,15 +79,18 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
fourth_position_match.fetch(:position)
|
fourth_position_match.fetch(:position)
|
||||||
])
|
])
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||||
expect(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq(
|
expect(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq(
|
||||||
group_match_positions(follower, group_number: first_follower_group.fetch(:number))
|
group_match_positions(follower, group_number: first_follower_group.fetch(:number))
|
||||||
)
|
)
|
||||||
expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq(
|
expect(group_match_positions(source,
|
||||||
|
group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq(
|
||||||
fourth_position_match.fetch(:position)
|
fourth_position_match.fetch(:position)
|
||||||
)
|
)
|
||||||
expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq(
|
expect(group_match_positions(source,
|
||||||
|
group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq(
|
||||||
first_position_match.fetch(:position)
|
first_position_match.fetch(:position)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -97,7 +103,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
expect(wrong_group_swap[:status]).to eq(422)
|
expect(wrong_group_swap[:status]).to eq(422)
|
||||||
expect(wrong_group_swap.dig(:json, :error)).to eq('Matches must be different group matches from the same group')
|
expect(wrong_group_swap.dig(:json, :error)).to eq('Matches must be different group matches from the same group')
|
||||||
source_after_invalid_swap = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source_after_invalid_swap = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id))
|
follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client,
|
||||||
|
tournament_id: follower.fetch(:id))
|
||||||
expect(tournament_signature(source_after_invalid_swap)).to eq(source_before_invalid_swap)
|
expect(tournament_signature(source_after_invalid_swap)).to eq(source_before_invalid_swap)
|
||||||
expect(tournament_signature(follower_after_invalid_swap)).to eq(follower_before_invalid_swap)
|
expect(tournament_signature(follower_after_invalid_swap)).to eq(follower_before_invalid_swap)
|
||||||
|
|
||||||
|
|
@ -116,7 +123,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
intermediate_round_participants_amount: 0
|
intermediate_round_participants_amount: 0
|
||||||
})
|
})
|
||||||
expect(update_cutoff[:status]).to eq(200)
|
expect(update_cutoff[:status]).to eq(200)
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
source_group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source_group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||||
|
|
@ -127,20 +134,25 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
group_team_map(source_group_stage.fetch(:groups).last).fetch(2).fetch(:name)
|
group_team_map(source_group_stage.fetch(:groups).last).fetch(2).fetch(:name)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise', range: 2..3))
|
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise',
|
||||||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', range: 3..4))
|
range: 2..3))
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise',
|
||||||
|
range: 3..4))
|
||||||
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
log_phase!('play group stage with deciders')
|
log_phase!('play group stage with deciders')
|
||||||
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) })
|
play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by do |group|
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
group.fetch(:number)
|
||||||
|
end)
|
||||||
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
log_phase!('verify playoff generation replicated')
|
log_phase!('verify playoff generation replicated')
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||||
expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1)
|
expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1)
|
||||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
|
||||||
expect(source.fetch(:team_action_lists)).to eq([])
|
expect(source.fetch(:team_action_lists)).to eq([])
|
||||||
expect(follower.fetch(:team_action_lists)).to eq([])
|
expect(follower.fetch(:team_action_lists)).to eq([])
|
||||||
|
|
||||||
|
|
@ -149,7 +161,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
expect(created_list.fetch(:key)).to eq(playoff_token_list_key)
|
expect(created_list.fetch(:key)).to eq(playoff_token_list_key)
|
||||||
|
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(playoff_token_list(source)).not_to be_nil
|
expect(playoff_token_list(source)).not_to be_nil
|
||||||
expect(playoff_token_list(follower)).not_to be_nil
|
expect(playoff_token_list(follower)).not_to be_nil
|
||||||
expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) })
|
||||||
|
|
@ -173,44 +186,54 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it')
|
log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it')
|
||||||
source_token_item = playoff_token_list(source).fetch(:team_action_items).first
|
source_token_item = playoff_token_list(source).fetch(:team_action_items).first
|
||||||
source_token_update = source_owner_client.patch(
|
source_token_update = source_owner_client.patch(
|
||||||
"/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(:team, :id)}",
|
"/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(
|
||||||
|
:team, :id
|
||||||
|
)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(source_token_update[:status]).to eq(200)
|
expect(source_token_update[:status]).to eq(200)
|
||||||
|
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(playoff_token_item_completed?(source, team_name: source_token_item.dig(:team, :name))).to eq(true)
|
expect(playoff_token_item_completed?(source, team_name: source_token_item.dig(:team, :name))).to eq(true)
|
||||||
expect(playoff_token_item_completed?(follower, team_name: source_token_item.dig(:team, :name))).to eq(true)
|
expect(playoff_token_item_completed?(follower, team_name: source_token_item.dig(:team, :name))).to eq(true)
|
||||||
|
|
||||||
follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name))
|
follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name))
|
||||||
locked_token_update = follower_owner_client.patch(
|
locked_token_update = follower_owner_client.patch(
|
||||||
"/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(:team, :id)}",
|
"/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(
|
||||||
|
:team, :id
|
||||||
|
)}",
|
||||||
body: { completed: false }
|
body: { completed: false }
|
||||||
)
|
)
|
||||||
expect(locked_token_update[:status]).to eq(423)
|
expect(locked_token_update[:status]).to eq(423)
|
||||||
expect(locked_token_update.dig(:json, :error)).to eq('Tournament is in read only follower mode')
|
expect(locked_token_update.dig(:json, :error)).to eq('Tournament is in read only follower mode')
|
||||||
|
|
||||||
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise', range: 2..4))
|
source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise',
|
||||||
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', range: 2..5))
|
range: 2..4))
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise',
|
||||||
|
range: 2..5))
|
||||||
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
log_phase!('finish playoff bracket')
|
log_phase!('finish playoff bracket')
|
||||||
finish_playoff_bracket!(source_id: source.fetch(:id))
|
finish_playoff_bracket!(source_id: source.fetch(:id))
|
||||||
exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise)
|
exercise_unrelated_requests!(source_noise:, follower_noise:)
|
||||||
|
|
||||||
log_phase!('verify final follower state and takeover')
|
log_phase!('verify final follower state and takeover')
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id))
|
||||||
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id))
|
follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id),
|
||||||
|
follower_tournament_id: follower.fetch(:id))
|
||||||
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
expect(tournament_signature(follower)).to eq(tournament_signature(source))
|
||||||
expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty
|
expect(synced_team_id_pairs(source:, follower:)).not_to be_empty
|
||||||
|
|
||||||
disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}", body: { read_only_mode: false })
|
disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}",
|
||||||
|
body: { read_only_mode: false })
|
||||||
expect(disable_follower[:status]).to eq(200)
|
expect(disable_follower[:status]).to eq(200)
|
||||||
|
|
||||||
follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id))
|
follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id))
|
||||||
writable_team = follower_after_disable.fetch(:teams).first
|
writable_team = follower_after_disable.fetch(:teams).first
|
||||||
takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}", body: { name: "#{writable_team.fetch(:name)} takeover" })
|
takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}",
|
||||||
|
body: { name: "#{writable_team.fetch(:name)} takeover" })
|
||||||
expect(takeover_update[:status]).to eq(200)
|
expect(takeover_update[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -220,8 +243,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client(base_url:, email:, password:)
|
def login_client(base_url:, email:, password:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.login!(email: email, password: password)
|
response = client.login!(email:, password:)
|
||||||
|
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
expect(client.authenticated?).to eq(true)
|
expect(client.authenticated?).to eq(true)
|
||||||
|
|
@ -232,11 +255,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
payload = {
|
payload = {
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Follower sync source tournament',
|
description: 'Follower sync source tournament',
|
||||||
public: public,
|
public:,
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
teams: 4.times.flat_map do |group_index|
|
teams: 4.times.flat_map do |group_index|
|
||||||
4.times.map do |seed_index|
|
Array.new(4) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -247,34 +270,34 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
response = client.post('/tournaments', body: payload)
|
response = client.post('/tournaments', body: payload)
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
fetch_tournament(client:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
def create_playoff_tournament(client:, public:, name_prefix:)
|
||||||
payload = {
|
payload = {
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Unrelated playoff tournament',
|
description: 'Unrelated playoff tournament',
|
||||||
public: public,
|
public:,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post('/tournaments', body: payload)
|
response = client.post('/tournaments', body: payload)
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
fetch_tournament(client:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_follower_tournament(client:, public:, name_prefix:)
|
def create_follower_tournament(client:, public:, name_prefix:)
|
||||||
response = client.post('/tournaments', body: {
|
response = client.post('/tournaments', body: {
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Follower tournament',
|
description: 'Follower tournament',
|
||||||
public: public,
|
public:,
|
||||||
read_only_mode: true,
|
read_only_mode: true,
|
||||||
sync_auth_token: sync_token
|
sync_auth_token: sync_token
|
||||||
})
|
})
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(client: client, tournament_id: response.dig(:json, :id))
|
fetch_tournament(client:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_tournament(client:, tournament_id:)
|
def fetch_tournament(client:, tournament_id:)
|
||||||
|
|
@ -298,9 +321,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
amount = rand(range)
|
amount = rand(range)
|
||||||
Array.new(amount) do |index|
|
Array.new(amount) do |index|
|
||||||
if index.even?
|
if index.even?
|
||||||
create_playoff_tournament(client: client, public: false, name_prefix: "#{prefix} Playoff #{index}")
|
create_playoff_tournament(client:, public: false, name_prefix: "#{prefix} Playoff #{index}")
|
||||||
else
|
else
|
||||||
create_group_stage_tournament(client: client, public: true, name_prefix: "#{prefix} Groups #{index}")
|
create_group_stage_tournament(client:, public: true, name_prefix: "#{prefix} Groups #{index}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -316,7 +339,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name })
|
update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name })
|
||||||
expect(update[:status]).to eq(200)
|
expect(update[:status]).to eq(200)
|
||||||
|
|
||||||
refreshed = fetch_tournament(client: client, tournament_id: tournament.fetch(:id))
|
refreshed = fetch_tournament(client:, tournament_id: tournament.fetch(:id))
|
||||||
team = refreshed.fetch(:teams).first
|
team = refreshed.fetch(:teams).first
|
||||||
return if team.nil?
|
return if team.nil?
|
||||||
|
|
||||||
|
|
@ -340,21 +363,21 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20)
|
def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
last_progress_at = Time.now
|
last_progress_at = Time.zone.now
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id)
|
source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id)
|
||||||
follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id)
|
follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id)
|
||||||
return follower if tournament_signature(source) == tournament_signature(follower)
|
return follower if tournament_signature(source) == tournament_signature(follower)
|
||||||
|
|
||||||
if Time.now - last_progress_at >= 2
|
if Time.zone.now - last_progress_at >= 2
|
||||||
puts "sync wait source=#{sync_progress_signature(source).inspect} follower=#{sync_progress_signature(follower).inspect}"
|
puts "sync wait source=#{sync_progress_signature(source).inspect} follower=#{sync_progress_signature(follower).inspect}"
|
||||||
$stdout.flush
|
$stdout.flush
|
||||||
last_progress_at = Time.now
|
last_progress_at = Time.zone.now
|
||||||
end
|
end
|
||||||
|
|
||||||
if Time.now >= deadline
|
if Time.zone.now >= deadline
|
||||||
raise <<~ERROR
|
raise <<~ERROR
|
||||||
Follower did not catch up within #{timeout}s
|
Follower did not catch up within #{timeout}s
|
||||||
source_signature=#{tournament_signature(source).inspect}
|
source_signature=#{tournament_signature(source).inspect}
|
||||||
|
|
@ -374,8 +397,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def synced_team_id_pairs(source:, follower:)
|
def synced_team_id_pairs(source:, follower:)
|
||||||
source_by_name = source.fetch(:teams).each_with_object({}) do |team, teams_by_name|
|
source_by_name = source.fetch(:teams).index_by do |team|
|
||||||
teams_by_name[team.fetch(:name)] = team
|
team.fetch(:name)
|
||||||
end
|
end
|
||||||
follower.fetch(:teams).filter_map do |team|
|
follower.fetch(:teams).filter_map do |team|
|
||||||
source_team = source_by_name[team.fetch(:name)]
|
source_team = source_by_name[team.fetch(:name)]
|
||||||
|
|
@ -400,7 +423,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
||||||
expect(stage_finish[:status]).to eq(422)
|
expect(stage_finish[:status]).to eq(422)
|
||||||
expect(stage_finish.dig(:json, :error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place')
|
expect(stage_finish.dig(:json,
|
||||||
|
:error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place')
|
||||||
expect(stage_finish.dig(:json, :blocking_ties).size).to be >= 2
|
expect(stage_finish.dig(:json, :blocking_ties).size).to be >= 2
|
||||||
|
|
||||||
stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index|
|
stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index|
|
||||||
|
|
@ -422,7 +446,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
expect(finish_decider[:status]).to eq(200)
|
expect(finish_decider[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' })
|
finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}",
|
||||||
|
body: { state: 'finished' })
|
||||||
expect(finish_stage_after_decider[:status]).to eq(200)
|
expect(finish_stage_after_decider[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -466,7 +491,10 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
playoff_matches = tournament.fetch(:stages)
|
playoff_matches = tournament.fetch(:stages)
|
||||||
.select { |stage| stage.fetch(:level) >= 0 }
|
.select { |stage| stage.fetch(:level) >= 0 }
|
||||||
.flat_map { |stage| stage.fetch(:matches) }
|
.flat_map { |stage| stage.fetch(:matches) }
|
||||||
.select { |match| %w[not_started in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2 }
|
.select do |match|
|
||||||
|
%w[not_started
|
||||||
|
in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2
|
||||||
|
end
|
||||||
.sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] }
|
.sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] }
|
||||||
break if playoff_matches.empty?
|
break if playoff_matches.empty?
|
||||||
|
|
||||||
|
|
@ -488,7 +516,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
||||||
[first_score, second_score].each_with_index do |points, index|
|
[first_score, second_score].each_with_index do |points, index|
|
||||||
response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points })
|
response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}",
|
||||||
|
body: { points: })
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -504,7 +533,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
||||||
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
||||||
timestamp: tournament[:timestamp],
|
timestamp: tournament[:timestamp],
|
||||||
teams: tournament.fetch(:teams).map { |team| team.slice(:name, :advancing_from_group_stage) }.sort_by { |team| team.fetch(:name) },
|
teams: tournament.fetch(:teams).map do |team|
|
||||||
|
team.slice(:name, :advancing_from_group_stage)
|
||||||
|
end.sort_by { |team| team.fetch(:name) },
|
||||||
team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list|
|
team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list|
|
||||||
{
|
{
|
||||||
key: list.fetch(:key),
|
key: list.fetch(:key),
|
||||||
|
|
@ -596,6 +627,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def playoff_token_item_completed?(tournament, team_name:)
|
def playoff_token_item_completed?(tournament, team_name:)
|
||||||
find_team_action_item(tournament, team_name: team_name).fetch(:completed)
|
find_team_action_item(tournament, team_name:).fetch(:completed)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
||||||
|
|
||||||
it 'broadcasts follower websocket updates after source sync import' do
|
it 'broadcasts follower websocket updates after source sync import' do
|
||||||
source = create_group_stage_tournament(client: source_client, base_url: source_base_url, name_prefix: 'Ws Sync Source')
|
source = create_group_stage_tournament(client: source_client, base_url: source_base_url,
|
||||||
|
name_prefix: 'Ws Sync Source')
|
||||||
follower = create_follower_tournament(name_prefix: 'Ws Sync Follower')
|
follower = create_follower_tournament(name_prefix: 'Ws Sync Follower')
|
||||||
|
|
||||||
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||||
|
|
@ -61,7 +62,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client(base_url:)
|
def login_client(base_url:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.login!(email: owner_email, password: owner_password)
|
response = client.login!(email: owner_email, password: owner_password)
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
client
|
client
|
||||||
|
|
@ -75,7 +76,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 4,
|
playoff_teams_amount: 4,
|
||||||
teams: 2.times.flat_map do |group_index|
|
teams: 2.times.flat_map do |group_index|
|
||||||
4.times.map do |seed_index|
|
Array.new(4) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -84,7 +85,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(base_url: base_url, tournament_id: response.dig(:json, :id))
|
fetch_tournament(base_url:, tournament_id: response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_follower_tournament(name_prefix:)
|
def create_follower_tournament(name_prefix:)
|
||||||
|
|
@ -101,7 +102,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_tournament(base_url:, tournament_id:)
|
def fetch_tournament(base_url:, tournament_id:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.get("/tournaments/#{tournament_id}")
|
response = client.get("/tournaments/#{tournament_id}")
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
|
|
@ -141,7 +142,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
|
|
||||||
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
||||||
[first_score, second_score].each_with_index do |points, index|
|
[first_score, second_score].each_with_index do |points, index|
|
||||||
score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points })
|
score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
|
||||||
expect(score_update[:status]).to eq(200)
|
expect(score_update[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -150,24 +151,24 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_list_presence!(tournament_id, timeout: 20)
|
def wait_for_list_presence!(tournament_id, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
follower = fetch_tournament(base_url: follower_base_url, tournament_id: tournament_id)
|
follower = fetch_tournament(base_url: follower_base_url, tournament_id:)
|
||||||
return follower if follower.fetch(:team_action_lists, []).any?
|
return follower if follower.fetch(:team_action_lists, []).any?
|
||||||
|
|
||||||
raise "follower list not present within #{timeout}s" if Time.now >= deadline
|
raise "follower list not present within #{timeout}s" if Time.zone.now >= deadline
|
||||||
|
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
|
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if team_action_item_completed?(payload, team_name: team_name) == completed
|
return payload if team_action_item_completed?(payload, team_name:) == completed
|
||||||
|
|
||||||
raise "follower websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "follower websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') }
|
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') }
|
||||||
let(:runner) do
|
let(:runner) do
|
||||||
TurniereE2E::ScenarioRunner.new(
|
TurniereE2E::ScenarioRunner.new(
|
||||||
base_url: base_url,
|
base_url:,
|
||||||
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
||||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
||||||
username: ENV['TURNIERE_E2E_USERNAME']
|
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16)
|
expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16)
|
||||||
|
|
||||||
playoffs_created = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
|
playoffs_created = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
|
||||||
playoff_entry_stage = playoffs_created.select { |stage| stage[:level] != -1 }.max_by { |stage| stage[:level] }
|
playoff_entry_stage = playoffs_created.reject { |stage| stage[:level] == -1 }.max_by { |stage| stage[:level] }
|
||||||
expect(playoff_entry_stage[:match_states].count).to eq(4)
|
expect(playoff_entry_stage[:match_states].count).to eq(4)
|
||||||
|
|
||||||
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt)
|
||||||
|
|
@ -49,7 +49,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
expect(created[:intermediate_round_participants_amount]).to eq(2)
|
expect(created[:intermediate_round_participants_amount]).to eq(2)
|
||||||
|
|
||||||
playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
|
playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries)
|
||||||
.select { |stage| stage[:level] != -1 }
|
.reject { |stage| stage[:level] == -1 }
|
||||||
.max_by { |stage| stage[:level] }
|
.max_by { |stage| stage[:level] }
|
||||||
expect(playoff_entry_stage[:state]).to eq('intermediate_stage')
|
expect(playoff_entry_stage[:state]).to eq('intermediate_stage')
|
||||||
expect(playoff_entry_stage[:match_states].count('single_team')).to eq(3)
|
expect(playoff_entry_stage[:match_states].count('single_team')).to eq(3)
|
||||||
|
|
@ -76,7 +76,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
expect(positions.fetch('G1-S2')).to eq(expected_positions[1])
|
expect(positions.fetch('G1-S2')).to eq(expected_positions[1])
|
||||||
|
|
||||||
if direct_result == :drawn
|
if direct_result == :drawn
|
||||||
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points))
|
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
|
||||||
|
:received_points))
|
||||||
.to eq(
|
.to eq(
|
||||||
team_name: 'G1-S1',
|
team_name: 'G1-S1',
|
||||||
position: 1,
|
position: 1,
|
||||||
|
|
@ -85,7 +86,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
scored_points: 4,
|
scored_points: 4,
|
||||||
received_points: 1
|
received_points: 1
|
||||||
)
|
)
|
||||||
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points))
|
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
|
||||||
|
:received_points))
|
||||||
.to eq(
|
.to eq(
|
||||||
team_name: 'G1-S2',
|
team_name: 'G1-S2',
|
||||||
position: 1,
|
position: 1,
|
||||||
|
|
@ -96,9 +98,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
expected_first_group_points = direct_result == :lost ? 9 : 6
|
expected_first_group_points = direct_result == :lost ? 9 : 6
|
||||||
expected_second_group_points = direct_result == :lost ? 6 : 6
|
expected_second_group_points = 6
|
||||||
|
|
||||||
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points))
|
expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
|
||||||
|
:received_points))
|
||||||
.to eq(
|
.to eq(
|
||||||
team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2',
|
team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2',
|
||||||
position: 1,
|
position: 1,
|
||||||
|
|
@ -107,7 +110,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
scored_points: 5,
|
scored_points: 5,
|
||||||
received_points: 2
|
received_points: 2
|
||||||
)
|
)
|
||||||
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points))
|
expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points,
|
||||||
|
:received_points))
|
||||||
.to eq(
|
.to eq(
|
||||||
team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1',
|
team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1',
|
||||||
position: 2,
|
position: 2,
|
||||||
|
|
@ -122,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do
|
||||||
|
|
||||||
[2, 3, 5, 8].each do |team_count|
|
[2, 3, 5, 8].each do |team_count|
|
||||||
it "drives a #{team_count}-team playoff-only tournament over HTTP to completion" do
|
it "drives a #{team_count}-team playoff-only tournament over HTTP to completion" do
|
||||||
result = runner.run_playoff_only(team_count: team_count)
|
result = runner.run_playoff_only(team_count:)
|
||||||
|
|
||||||
completed_stages = result.dig(:checkpoints, :completed, :stage_summaries)
|
completed_stages = result.dig(:checkpoints, :completed, :stage_summaries)
|
||||||
final_stage = completed_stages.find { |stage| stage[:level] == 0 }
|
final_stage = completed_stages.find { |stage| stage[:level] == 0 }
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,20 @@ RSpec.describe 'Tournament rendering HTTP E2E' do
|
||||||
let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' }
|
let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' }
|
||||||
let(:runner) do
|
let(:runner) do
|
||||||
TurniereE2E::ScenarioRunner.new(
|
TurniereE2E::ScenarioRunner.new(
|
||||||
base_url: base_url,
|
base_url:,
|
||||||
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
||||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
||||||
username: ENV['TURNIERE_E2E_USERNAME']
|
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, max_server_duration_ms: 1000 },
|
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000,
|
||||||
{ group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000, max_server_duration_ms: 5000 },
|
max_server_duration_ms: 1000 },
|
||||||
{ group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000, max_server_duration_ms: 5000 }
|
{ group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000,
|
||||||
|
max_server_duration_ms: 5000 },
|
||||||
|
{ group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000,
|
||||||
|
max_server_duration_ms: 5000 }
|
||||||
].each do |scenario|
|
].each do |scenario|
|
||||||
it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do
|
it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do
|
||||||
result = runner.run_group_stage_render_profile(
|
result = runner.run_group_stage_render_profile(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||||
let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" }
|
let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" }
|
||||||
let(:client) { login_client }
|
let(:client) { login_client }
|
||||||
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) }
|
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
|
||||||
|
|
||||||
it 'roundtrips current snapshot into follower tournament through test_sync endpoint' do
|
it 'roundtrips current snapshot into follower tournament through test_sync endpoint' do
|
||||||
follower = create_follower_tournament(name_prefix: 'Sync Test Follower')
|
follower = create_follower_tournament(name_prefix: 'Sync Test Follower')
|
||||||
|
|
@ -74,7 +74,8 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
expect(update_by_item_id[:status]).to eq(200)
|
expect(update_by_item_id[:status]).to eq(200)
|
||||||
|
|
||||||
update_by_business_key = client.patch(
|
update_by_business_key = client.patch(
|
||||||
"/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}",
|
"/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team,
|
||||||
|
:id)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(update_by_business_key[:status]).to eq(200)
|
expect(update_by_business_key[:status]).to eq(200)
|
||||||
|
|
@ -118,7 +119,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client
|
def login_client
|
||||||
api_client = TurniereE2E::ApiClient.new(base_url: base_url)
|
api_client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = api_client.login!(email: owner_email, password: owner_password)
|
response = api_client.login!(email: owner_email, password: owner_password)
|
||||||
|
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
|
|
@ -133,7 +134,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 4,
|
playoff_teams_amount: 4,
|
||||||
teams: 2.times.flat_map do |group_index|
|
teams: 2.times.flat_map do |group_index|
|
||||||
4.times.map do |seed_index|
|
Array.new(4) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -141,10 +142,10 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
body[:sync_target_url] = sync_target_url unless sync_target_url.nil? || sync_target_url.empty?
|
body[:sync_target_url] = sync_target_url if sync_target_url.present?
|
||||||
body[:sync_auth_token] = sync_auth_token unless sync_auth_token.nil? || sync_auth_token.empty?
|
body[:sync_auth_token] = sync_auth_token if sync_auth_token.present?
|
||||||
|
|
||||||
response = client.post('/tournaments', body: body)
|
response = client.post('/tournaments', body:)
|
||||||
expect(response[:status]).to eq(201)
|
expect(response[:status]).to eq(201)
|
||||||
fetch_tournament(response.dig(:json, :id))
|
fetch_tournament(response.dig(:json, :id))
|
||||||
end
|
end
|
||||||
|
|
@ -201,7 +202,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
|
|
||||||
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
||||||
[first_score, second_score].each_with_index do |points, index|
|
[first_score, second_score].each_with_index do |points, index|
|
||||||
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points })
|
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
|
||||||
expect(score_update[:status]).to eq(200)
|
expect(score_update[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
||||||
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||||
let(:client) { login_client }
|
let(:client) { login_client }
|
||||||
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) }
|
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
|
||||||
|
|
||||||
it 'broadcasts initial and updated team action list state over websocket' do
|
it 'broadcasts initial and updated team action list state over websocket' do
|
||||||
tournament = create_group_stage_tournament(name_prefix: 'Ws Leader')
|
tournament = create_group_stage_tournament(name_prefix: 'Ws Leader')
|
||||||
|
|
@ -30,7 +30,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
tournament = fetch_tournament(tournament.fetch(:id))
|
tournament = fetch_tournament(tournament.fetch(:id))
|
||||||
token_list = find_playoff_token_list!(tournament)
|
token_list = find_playoff_token_list!(tournament)
|
||||||
|
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id))
|
cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id))
|
||||||
|
|
||||||
initial_payload = cable_client.wait_for_payload!
|
initial_payload = cable_client.wait_for_payload!
|
||||||
|
|
@ -50,7 +50,8 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true)
|
expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true)
|
||||||
|
|
||||||
update_by_business_key = client.patch(
|
update_by_business_key = client.patch(
|
||||||
"/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}",
|
"/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team,
|
||||||
|
:id)}",
|
||||||
body: { completed: true }
|
body: { completed: true }
|
||||||
)
|
)
|
||||||
expect(update_by_business_key[:status]).to eq(200)
|
expect(update_by_business_key[:status]).to eq(200)
|
||||||
|
|
@ -67,7 +68,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client
|
def login_client
|
||||||
api_client = TurniereE2E::ApiClient.new(base_url: base_url)
|
api_client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = api_client.login!(email: owner_email, password: owner_password)
|
response = api_client.login!(email: owner_email, password: owner_password)
|
||||||
|
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
|
|
@ -82,7 +83,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 4,
|
playoff_teams_amount: 4,
|
||||||
teams: 2.times.flat_map do |group_index|
|
teams: 2.times.flat_map do |group_index|
|
||||||
4.times.map do |seed_index|
|
Array.new(4) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -134,7 +135,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
|
|
||||||
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) }
|
||||||
[first_score, second_score].each_with_index do |points, index|
|
[first_score, second_score].each_with_index do |points, index|
|
||||||
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points })
|
score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: })
|
||||||
expect(score_update[:status]).to eq(200)
|
expect(score_update[:status]).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -143,12 +144,12 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
|
def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if team_action_item_completed?(payload, team_name: team_name) == completed
|
return payload if team_action_item_completed?(payload, team_name:) == completed
|
||||||
|
|
||||||
raise "team action websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "team action websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client(base_url:)
|
def login_client(base_url:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.login!(email: owner_email, password: owner_password)
|
response = client.login!(email: owner_email, password: owner_password)
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
client
|
client
|
||||||
|
|
@ -74,7 +74,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
||||||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
description: 'Follower websocket timer source tournament',
|
description: 'Follower websocket timer source tournament',
|
||||||
public: false,
|
public: false,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
@ -96,12 +96,12 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if payload.fetch('timer_mode') == timer_mode
|
return payload if payload.fetch('timer_mode') == timer_mode
|
||||||
|
|
||||||
raise "follower timer websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "follower timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
||||||
it 'broadcasts timer state changes over websocket' do
|
it 'broadcasts timer state changes over websocket' do
|
||||||
tournament = create_playoff_tournament(name_prefix: 'Ws Timer')
|
tournament = create_playoff_tournament(name_prefix: 'Ws Timer')
|
||||||
|
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id))
|
cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id))
|
||||||
|
|
||||||
initial_payload = cable_client.wait_for_timer_payload!
|
initial_payload = cable_client.wait_for_timer_payload!
|
||||||
|
|
@ -54,7 +54,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client
|
def login_client
|
||||||
api_client = TurniereE2E::ApiClient.new(base_url: base_url)
|
api_client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = api_client.login!(email: owner_email, password: owner_password)
|
response = api_client.login!(email: owner_email, password: owner_password)
|
||||||
|
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
|
|
@ -66,7 +66,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
||||||
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
description: 'Websocket timer tournament',
|
description: 'Websocket timer tournament',
|
||||||
public: false,
|
public: false,
|
||||||
teams: 4.times.map do |index|
|
teams: Array.new(4) do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-#{index + 1}" }
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
@ -75,12 +75,12 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = Time.zone.now + timeout
|
||||||
loop do
|
loop do
|
||||||
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max)
|
payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max)
|
||||||
return payload if payload.fetch('timer_mode') == timer_mode
|
return payload if payload.fetch('timer_mode') == timer_mode
|
||||||
|
|
||||||
raise "timer websocket payload not received within #{timeout}s" if Time.now >= deadline
|
raise "timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ FactoryBot.define do
|
||||||
name { Beamer::DEFAULT_NAME }
|
name { Beamer::DEFAULT_NAME }
|
||||||
|
|
||||||
initialize_with do
|
initialize_with do
|
||||||
tournament.beamers.find_or_initialize_by(name: name)
|
tournament.beamers.find_or_initialize_by(name:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ FactoryBot.define do
|
||||||
stage
|
stage
|
||||||
|
|
||||||
after(:create) do |group, evaluator|
|
after(:create) do |group, evaluator|
|
||||||
create_list(evaluator.match_factory, evaluator.match_count, group: group)
|
create_list(evaluator.match_factory, evaluator.match_count, group:)
|
||||||
group.group_scores = group.teams.map do |team|
|
group.group_scores = group.teams.map do |team|
|
||||||
create(:group_score, team: team, group: group)
|
create(:group_score, team:, group:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,14 @@ FactoryBot.define do
|
||||||
intermediate_round_participants_amount { 0 }
|
intermediate_round_participants_amount { 0 }
|
||||||
end
|
end
|
||||||
after(:create) do |tournament, evaluator|
|
after(:create) do |tournament, evaluator|
|
||||||
if evaluator.teams.present?
|
tournament.teams = (evaluator.teams.presence || create_list(:team, evaluator.teams_count, tournament:))
|
||||||
tournament.teams = evaluator.teams
|
|
||||||
else
|
|
||||||
tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament)
|
|
||||||
end
|
|
||||||
tournament.playoff_teams_amount = evaluator.playoff_teams_amount
|
tournament.playoff_teams_amount = evaluator.playoff_teams_amount
|
||||||
tournament.instant_finalists_amount = evaluator.instant_finalists_amount
|
tournament.instant_finalists_amount = evaluator.instant_finalists_amount
|
||||||
tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount
|
tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount
|
||||||
if tournament.playoff_teams_amount != tournament.instant_finalists_amount + tournament.intermediate_round_participants_amount / 2
|
if tournament.playoff_teams_amount != tournament.instant_finalists_amount + (tournament.intermediate_round_participants_amount / 2)
|
||||||
raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2'
|
raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2'
|
||||||
end
|
end
|
||||||
|
|
||||||
tournament.save!
|
tournament.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -36,7 +33,7 @@ FactoryBot.define do
|
||||||
(1..evaluator.stage_count).each do |level|
|
(1..evaluator.stage_count).each do |level|
|
||||||
tournament.stages << create(
|
tournament.stages << create(
|
||||||
:playoff_stage,
|
:playoff_stage,
|
||||||
level: level,
|
level:,
|
||||||
match_count: -1,
|
match_count: -1,
|
||||||
match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match
|
match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ RSpec.describe AddGroupStageToTournament, type: :interactor do
|
||||||
@empty_tournament = create(:stageless_tournament)
|
@empty_tournament = create(:stageless_tournament)
|
||||||
@group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
|
@group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0)
|
||||||
@group_stage = create(:group_stage)
|
@group_stage = create(:group_stage)
|
||||||
@groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values
|
@groups = { 1 => create_list(:team, 4), 2 => create_list(:team, 4) }.values
|
||||||
@tournament_service_defaults = [78_345, 2_387]
|
@tournament_service_defaults = [78_345, 2_387]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ RSpec.describe Beamer, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows same name in different tournaments' do
|
it 'allows same name in different tournaments' do
|
||||||
beamer = create(:beamer, name: 'projector')
|
create(:beamer, name: 'projector')
|
||||||
other = build(:beamer, name: 'projector', tournament: create(:tournament))
|
other = build(:beamer, name: 'projector', tournament: create(:tournament))
|
||||||
expect(other).to be_valid
|
expect(other).to be_valid
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,21 @@ RSpec.describe GroupScore, type: :model do
|
||||||
let(:team_c) { create(:team) }
|
let(:team_c) { create(:team) }
|
||||||
|
|
||||||
let(:group_score_a) do
|
let(:group_score_a) do
|
||||||
create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
|
create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
|
||||||
end
|
end
|
||||||
let(:group_score_b) do
|
let(:group_score_b) do
|
||||||
create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 7, received_points: 4)
|
create(:group_score, group:, team: team_b, group_points: 6, scored_points: 7, received_points: 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'prefers the winner of the direct comparison when the first three criteria are tied' do
|
it 'prefers the winner of the direct comparison when the first three criteria are tied' do
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 3)
|
create(:match_score, match:, team: team_a, points: 3)
|
||||||
create(:match_score, match: match, team: team_c, points: 1)
|
create(:match_score, match:, team: team_c, points: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 4)
|
create(:match_score, match:, team: team_a, points: 4)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
group_score_a.reload
|
group_score_a.reload
|
||||||
|
|
@ -40,9 +40,9 @@ RSpec.describe GroupScore, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps the teams tied when their direct comparison was a draw' do
|
it 'keeps the teams tied when their direct comparison was a draw' do
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
group_score_a.reload
|
group_score_a.reload
|
||||||
|
|
@ -59,10 +59,10 @@ RSpec.describe GroupScore, type: :model do
|
||||||
let(:team_b) { create(:team) }
|
let(:team_b) { create(:team) }
|
||||||
|
|
||||||
let(:group_score_a) do
|
let(:group_score_a) do
|
||||||
create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
|
create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4)
|
||||||
end
|
end
|
||||||
let(:group_score_b) do
|
let(:group_score_b) do
|
||||||
create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 7, received_points: 4)
|
create(:group_score, group:, team: team_b, group_points: 3, scored_points: 7, received_points: 4)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'reports the first deciding metric when points are different' do
|
it 'reports the first deciding metric when points are different' do
|
||||||
|
|
@ -78,7 +78,7 @@ RSpec.describe GroupScore, type: :model do
|
||||||
|
|
||||||
it 'reports the direct comparison result when head-to-head resolves the tie' do
|
it 'reports the direct comparison result when head-to-head resolves the tie' do
|
||||||
group_score_b.update!(group_points: 6)
|
group_score_b.update!(group_points: 6)
|
||||||
match = create(:group_match, group: group, state: :finished).tap do |created_match|
|
match = create(:group_match, group:, state: :finished).tap do |created_match|
|
||||||
create(:match_score, match: created_match, team: team_a, points: 3)
|
create(:match_score, match: created_match, team: team_a, points: 3)
|
||||||
create(:match_score, match: created_match, team: team_b, points: 1)
|
create(:match_score, match: created_match, team: team_b, points: 1)
|
||||||
end
|
end
|
||||||
|
|
@ -95,7 +95,7 @@ RSpec.describe GroupScore, type: :model do
|
||||||
|
|
||||||
it 'reports an unresolved tie when head-to-head was a draw' do
|
it 'reports an unresolved tie when head-to-head was a draw' do
|
||||||
group_score_b.update!(group_points: 6)
|
group_score_b.update!(group_points: 6)
|
||||||
match = create(:group_match, group: group, state: :finished).tap do |created_match|
|
match = create(:group_match, group:, state: :finished).tap do |created_match|
|
||||||
create(:match_score, match: created_match, team: team_a, points: 2)
|
create(:match_score, match: created_match, team: team_a, points: 2)
|
||||||
create(:match_score, match: created_match, team: team_b, points: 2)
|
create(:match_score, match: created_match, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
@ -112,11 +112,11 @@ RSpec.describe GroupScore, type: :model do
|
||||||
|
|
||||||
it 'falls back to hidden points when direct comparison is still tied' do
|
it 'falls back to hidden points when direct comparison is still tied' do
|
||||||
group_score_b.update!(group_points: 6)
|
group_score_b.update!(group_points: 6)
|
||||||
direct_match = create(:group_match, group: group, state: :finished).tap do |created_match|
|
direct_match = create(:group_match, group:, state: :finished).tap do |created_match|
|
||||||
create(:match_score, match: created_match, team: team_a, points: 2)
|
create(:match_score, match: created_match, team: team_a, points: 2)
|
||||||
create(:match_score, match: created_match, team: team_b, points: 2)
|
create(:match_score, match: created_match, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
decider_match = create(:group_match, group: group, state: :finished, decider: true).tap do |created_match|
|
decider_match = create(:group_match, group:, state: :finished, decider: true).tap do |created_match|
|
||||||
create(:match_score, match: created_match, team: team_a, hidden_points: 3)
|
create(:match_score, match: created_match, team: team_a, hidden_points: 3)
|
||||||
create(:match_score, match: created_match, team: team_b, hidden_points: 1)
|
create(:match_score, match: created_match, team: team_b, hidden_points: 1)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ rescue ActiveRecord::PendingMigrationError => e
|
||||||
end
|
end
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
config.fixture_path = Rails.root.join('spec/fixtures').to_s
|
||||||
|
|
||||||
# Run only focused tests
|
# Run only focused tests
|
||||||
# TODO REVERT ME
|
# TODO REVERT ME
|
||||||
|
|
|
||||||
|
|
@ -35,5 +35,5 @@ RSpec.describe TournamentsController, type: :routing do
|
||||||
it 'routes to #test_sync' do
|
it 'routes to #test_sync' do
|
||||||
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ RSpec.describe GroupStageService do
|
||||||
before do
|
before do
|
||||||
@teams1 = create_list(:team, 4)
|
@teams1 = create_list(:team, 4)
|
||||||
@teams2 = create_list(:team, 4)
|
@teams2 = create_list(:team, 4)
|
||||||
@prepared_groups = Hash[1 => @teams1, 2 => @teams2].values
|
@prepared_groups = { 1 => @teams1, 2 => @teams2 }.values
|
||||||
end
|
end
|
||||||
describe '#generate_group_stage' do
|
describe '#generate_group_stage' do
|
||||||
let(:prepared_groups_groupstage) do
|
let(:prepared_groups_groupstage) do
|
||||||
|
|
@ -52,7 +52,7 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO test get_advancing_teams when test data for running group stage is ready
|
# TODO: test get_advancing_teams when test data for running group stage is ready
|
||||||
|
|
||||||
describe '#get_group_object_from' do
|
describe '#get_group_object_from' do
|
||||||
it 'returns a group' do
|
it 'returns a group' do
|
||||||
|
|
@ -205,13 +205,13 @@ RSpec.describe GroupStageService do
|
||||||
team_a = create(:team)
|
team_a = create(:team)
|
||||||
team_b = create(:team)
|
team_b = create(:team)
|
||||||
|
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 4)
|
create(:match_score, match:, team: team_a, points: 4)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
|
create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
|
||||||
create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
|
create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
|
||||||
|
|
||||||
expect(GroupStageService.teams_sorted_by_group_scores(group.reload)).to eq([team_a, team_b])
|
expect(GroupStageService.teams_sorted_by_group_scores(group.reload)).to eq([team_a, team_b])
|
||||||
end
|
end
|
||||||
|
|
@ -224,14 +224,14 @@ RSpec.describe GroupStageService do
|
||||||
playoff_teams_amount: 2,
|
playoff_teams_amount: 2,
|
||||||
instant_finalists_amount: 2,
|
instant_finalists_amount: 2,
|
||||||
intermediate_round_participants_amount: 0,
|
intermediate_round_participants_amount: 0,
|
||||||
group_stage: group_stage)
|
group_stage:)
|
||||||
end
|
end
|
||||||
let(:group_stage) { create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0) }
|
let(:group_stage) { create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0) }
|
||||||
let(:group) { create(:group, stage: group_stage, match_count: 0, number: 1) }
|
let(:group) { create(:group, stage: group_stage, match_count: 0, number: 1) }
|
||||||
let(:team_a) { create(:team, tournament: tournament, name: 'A') }
|
let(:team_a) { create(:team, tournament:, name: 'A') }
|
||||||
let(:team_b) { create(:team, tournament: tournament, name: 'B') }
|
let(:team_b) { create(:team, tournament:, name: 'B') }
|
||||||
let(:team_c) { create(:team, tournament: tournament, name: 'C') }
|
let(:team_c) { create(:team, tournament:, name: 'C') }
|
||||||
let(:team_d) { create(:team, tournament: tournament, name: 'D') }
|
let(:team_d) { create(:team, tournament:, name: 'D') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
group_stage.groups = [group]
|
group_stage.groups = [group]
|
||||||
|
|
@ -240,13 +240,13 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns direct-comparison metadata for teams separated by head-to-head' do
|
it 'returns direct-comparison metadata for teams separated by head-to-head' do
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 4)
|
create(:match_score, match:, team: team_a, points: 4)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
|
score_a = create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2)
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
|
score_b = create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -269,17 +269,18 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do
|
it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do
|
||||||
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||||
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -301,17 +302,18 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'requests a decider when tied teams both advance but their seeding stays unresolved' do
|
it 'requests a decider when tied teams both advance but their seeding stays unresolved' do
|
||||||
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||||
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -333,17 +335,18 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do
|
it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do
|
||||||
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
||||||
create(:group_match, group: group, state: :in_progress).tap do |match|
|
create(:group_match, group:, state: :in_progress).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 1, scored_points: 2, received_points: 2)
|
score_a = create(:group_score, group:, team: team_a, group_points: 1, scored_points: 2, received_points: 2)
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 1, scored_points: 2, received_points: 2)
|
score_b = create(:group_score, group:, team: team_b, group_points: 1, scored_points: 2, received_points: 2)
|
||||||
score_c = create(:group_score, group: group, team: team_c, group_points: 0, scored_points: 0, received_points: 0)
|
score_c = create(:group_score, group:, team: team_c, group_points: 0, scored_points: 0, received_points: 0)
|
||||||
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 0, received_points: 0)
|
score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 0, received_points: 0)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -352,12 +355,15 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not expose unresolved tie metadata outside the advancing cutoff before the group is finished' do
|
it 'does not expose unresolved tie metadata outside the advancing cutoff before the group is finished' do
|
||||||
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 9, scored_points: 5, received_points: 1, position: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 9, scored_points: 5, received_points: 1,
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 4, received_points: 2, position: 2)
|
position: 1)
|
||||||
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
score_b = create(:group_score, group:, team: team_b, group_points: 6, scored_points: 4, received_points: 2,
|
||||||
score_d = create(:group_score, group: group, team: team_d, group_points: 3, scored_points: 2, received_points: 3)
|
position: 2)
|
||||||
|
score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||||
|
score_d = create(:group_score, group:, team: team_d, group_points: 3, scored_points: 2, received_points: 3)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -367,21 +373,22 @@ RSpec.describe GroupStageService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'uses the decider hidden points to resolve a cutoff tie' do
|
it 'uses the decider hidden points to resolve a cutoff tie' do
|
||||||
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)
|
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1,
|
||||||
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
||||||
create(:group_match, group: group, state: :finished).tap do |match|
|
create(:group_match, group:, state: :finished).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, points: 2)
|
create(:match_score, match:, team: team_a, points: 2)
|
||||||
create(:match_score, match: match, team: team_b, points: 2)
|
create(:match_score, match:, team: team_b, points: 2)
|
||||||
end
|
end
|
||||||
create(:group_match, group: group, state: :finished, decider: true).tap do |match|
|
create(:group_match, group:, state: :finished, decider: true).tap do |match|
|
||||||
create(:match_score, match: match, team: team_a, hidden_points: 5)
|
create(:match_score, match:, team: team_a, hidden_points: 5)
|
||||||
create(:match_score, match: match, team: team_b, hidden_points: 3)
|
create(:match_score, match:, team: team_b, hidden_points: 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
|
||||||
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
|
||||||
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
|
||||||
|
|
||||||
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
|
||||||
|
|
||||||
|
|
@ -417,7 +424,7 @@ RSpec.describe GroupStageService do
|
||||||
|
|
||||||
@tournament = create(:prepared_group_stage_tournament,
|
@tournament = create(:prepared_group_stage_tournament,
|
||||||
group_stage: @group_stage,
|
group_stage: @group_stage,
|
||||||
teams: teams,
|
teams:,
|
||||||
playoff_teams_amount: 16,
|
playoff_teams_amount: 16,
|
||||||
instant_finalists_amount: 16,
|
instant_finalists_amount: 16,
|
||||||
intermediate_round_participants_amount: 0)
|
intermediate_round_participants_amount: 0)
|
||||||
|
|
@ -427,7 +434,7 @@ RSpec.describe GroupStageService do
|
||||||
match.match_scores.each do |ms|
|
match.match_scores.each do |ms|
|
||||||
# give the team 10 points minus the number in their name
|
# give the team 10 points minus the number in their name
|
||||||
# this results in the team 0 always winning and getting to place 1 in the group etc.
|
# this results in the team 0 always winning and getting to place 1 in the group etc.
|
||||||
ms.points = 10 - ms.team.name.split(' ').last.to_i
|
ms.points = 10 - ms.team.name.split.last.to_i
|
||||||
ms.save!
|
ms.save!
|
||||||
end
|
end
|
||||||
match.state = 'finished'
|
match.state = 'finished'
|
||||||
|
|
@ -450,7 +457,7 @@ RSpec.describe GroupStageService do
|
||||||
advancing_teams.each_with_index do |team, i|
|
advancing_teams.each_with_index do |team, i|
|
||||||
# if index is even, the team should be of a first place; end in a 0
|
# if index is even, the team should be of a first place; end in a 0
|
||||||
# if index is odd, the team should be of a second place; end in a 1
|
# if index is odd, the team should be of a second place; end in a 1
|
||||||
team_quality = team.name.split(' ').last.to_i
|
team_quality = team.name.split.last.to_i
|
||||||
expect(team_quality % 2).to be(i % 2)
|
expect(team_quality % 2).to be(i % 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -469,9 +476,9 @@ RSpec.describe GroupStageService do
|
||||||
advancing_teams = GroupStageService.get_advancing_teams(@group_stage)
|
advancing_teams = GroupStageService.get_advancing_teams(@group_stage)
|
||||||
advancing_teams.each_slice(2).to_a.each do |matchup|
|
advancing_teams.each_slice(2).to_a.each do |matchup|
|
||||||
# this is the team that landed a first place in the group
|
# this is the team that landed a first place in the group
|
||||||
first_place_team = matchup[0].name.split(' ')[1].to_i
|
first_place_team = matchup[0].name.split[1].to_i
|
||||||
# this is the team that landed a second place in the group
|
# this is the team that landed a second place in the group
|
||||||
second_place_team = matchup[1].name.split(' ')[1].to_i
|
second_place_team = matchup[1].name.split[1].to_i
|
||||||
expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team)
|
expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ RSpec.describe MatchService do
|
||||||
first_team = match.match_scores.first.team.name
|
first_team = match.match_scores.first.team.name
|
||||||
second_team = match.match_scores.second.team.name
|
second_team = match.match_scores.second.team.name
|
||||||
expect(first_team).to eq(teams[2 * index].name)
|
expect(first_team).to eq(teams[2 * index].name)
|
||||||
expect(second_team).to eq(teams[2 * index + 1].name)
|
expect(second_team).to eq(teams[(2 * index) + 1].name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ RSpec.describe PlayoffStageService do
|
||||||
{ team_size: 9, expected_amount_of_playoff_stages: 4 },
|
{ team_size: 9, expected_amount_of_playoff_stages: 4 },
|
||||||
{ team_size: 10, expected_amount_of_playoff_stages: 4 },
|
{ team_size: 10, expected_amount_of_playoff_stages: 4 },
|
||||||
{ team_size: 16, expected_amount_of_playoff_stages: 4 },
|
{ team_size: 16, expected_amount_of_playoff_stages: 4 },
|
||||||
{ team_size: 24, expected_amount_of_playoff_stages: 5 },
|
{ team_size: 24, expected_amount_of_playoff_stages: 5 }
|
||||||
].each do |parameters|
|
].each do |parameters|
|
||||||
it "generates playoff stages for #{parameters[:team_size]} teams" do
|
it "generates playoff stages for #{parameters[:team_size]} teams" do
|
||||||
amount_of_teams = parameters[:team_size]
|
amount_of_teams = parameters[:team_size]
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ RSpec.describe TeamActionListService do
|
||||||
action_name: 'Break',
|
action_name: 'Break',
|
||||||
source: 'unknown_source'
|
source: 'unknown_source'
|
||||||
)
|
)
|
||||||
end.to raise_error(TeamActionListService::InvalidSourceError, 'Unsupported team action list source: unknown_source')
|
end.to raise_error(TeamActionListService::InvalidSourceError,
|
||||||
|
'Unsupported team action list source: unknown_source')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises when source resolves to no teams' do
|
it 'raises when source resolves to no teams' do
|
||||||
|
|
@ -62,7 +63,8 @@ RSpec.describe TeamActionListService do
|
||||||
action_name: 'Nothing',
|
action_name: 'Nothing',
|
||||||
source: 'all_tournament_teams'
|
source: 'all_tournament_teams'
|
||||||
)
|
)
|
||||||
end.to raise_error(TeamActionListService::EmptySourceError, 'Source all_tournament_teams does not currently contain any teams')
|
end.to raise_error(TeamActionListService::EmptySourceError,
|
||||||
|
'Source all_tournament_teams does not currently contain any teams')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'Tournament live payloads' do
|
||||||
expect(serialized_match[:position]).to eq(match.position)
|
expect(serialized_match[:position]).to eq(match.position)
|
||||||
expect(serialized_match.dig(:group, :id)).to eq(match.group_id)
|
expect(serialized_match.dig(:group, :id)).to eq(match.group_id)
|
||||||
expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id)
|
expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id)
|
||||||
expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:teams].pluck(:id)).to match_array(match.teams.map(&:id))
|
||||||
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
|
|
||||||
snapshot = TournamentSnapshotBuilder.build(source)
|
snapshot = TournamentSnapshotBuilder.build(source)
|
||||||
|
|
||||||
described_class.import!(tournament: follower, snapshot: snapshot)
|
described_class.import!(tournament: follower, snapshot:)
|
||||||
|
|
||||||
follower.reload
|
follower.reload
|
||||||
expect(follower.sync_source_tournament_id).to eq(source.id)
|
expect(follower.sync_source_tournament_id).to eq(source.id)
|
||||||
|
|
@ -25,8 +25,12 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count)
|
expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count)
|
||||||
expect(follower.stages.count).to eq(source.stages.count)
|
expect(follower.stages.count).to eq(source.stages.count)
|
||||||
expect(follower.matches.count).to eq(source.matches.count)
|
expect(follower.matches.count).to eq(source.matches.count)
|
||||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) })
|
expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map do |team|
|
||||||
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) })
|
team.fetch(:id)
|
||||||
|
end)
|
||||||
|
expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map do |beamer|
|
||||||
|
beamer.fetch(:id)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'ignores stale snapshots that arrive after a newer one' do
|
it 'ignores stale snapshots that arrive after a newer one' do
|
||||||
|
|
@ -54,7 +58,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
|
|
||||||
snapshot = TournamentSnapshotBuilder.build(source)
|
snapshot = TournamentSnapshotBuilder.build(source)
|
||||||
|
|
||||||
described_class.import!(tournament: follower, snapshot: snapshot)
|
described_class.import!(tournament: follower, snapshot:)
|
||||||
|
|
||||||
follower.reload
|
follower.reload
|
||||||
expect(follower.id).not_to eq(source.id)
|
expect(follower.id).not_to eq(source.id)
|
||||||
|
|
@ -62,7 +66,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
expect(follower.teams.count).to eq(128)
|
expect(follower.teams.count).to eq(128)
|
||||||
expect(follower.stages.where(level: -1).first.groups.count).to eq(32)
|
expect(follower.stages.where(level: -1).first.groups.count).to eq(32)
|
||||||
expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(64)
|
expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(64)
|
||||||
expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count)
|
expect(follower.matches.count(&:decider?)).to eq(source.matches.count(&:decider?))
|
||||||
expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id))
|
expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id))
|
||||||
expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
|
expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id))
|
||||||
end
|
end
|
||||||
|
|
@ -95,15 +99,15 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
logo_url: '/logos/stream-b.png'
|
logo_url: '/logos/stream-b.png'
|
||||||
)
|
)
|
||||||
|
|
||||||
group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished)
|
group_stage = Stage.create!(tournament:, level: -1, state: :finished)
|
||||||
advancing_teams = []
|
advancing_teams = []
|
||||||
|
|
||||||
32.times do |group_index|
|
32.times do |group_index|
|
||||||
group = Group.create!(stage: group_stage, number: group_index + 1)
|
group = Group.create!(stage: group_stage, number: group_index + 1)
|
||||||
teams = 4.times.map do |seed_index|
|
teams = Array.new(4) do |seed_index|
|
||||||
create(
|
create(
|
||||||
:team,
|
:team,
|
||||||
tournament: tournament,
|
tournament:,
|
||||||
name: "Leader-G#{group_index + 1}-S#{seed_index + 1}"
|
name: "Leader-G#{group_index + 1}-S#{seed_index + 1}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -111,17 +115,15 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero?
|
teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero?
|
||||||
teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15
|
teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15
|
||||||
|
|
||||||
create_group_round_robin!(group: group, teams: teams)
|
create_group_round_robin!(group:, teams:)
|
||||||
create_group_scores!(group: group, teams: teams)
|
create_group_scores!(group:, teams:)
|
||||||
|
|
||||||
if group_index < 3
|
create_decider_match!(group:, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) if group_index < 3
|
||||||
create_decider_match!(group: group, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index])
|
|
||||||
end
|
|
||||||
|
|
||||||
advancing_teams.concat(teams.take(2))
|
advancing_teams.concat(teams.take(2))
|
||||||
end
|
end
|
||||||
|
|
||||||
result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams)
|
result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: advancing_teams)
|
||||||
expect(result).to be_a_success
|
expect(result).to be_a_success
|
||||||
|
|
||||||
tournament.reload
|
tournament.reload
|
||||||
|
|
@ -140,23 +142,23 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
]
|
]
|
||||||
|
|
||||||
pairings.each_with_index do |(left_index, right_index, left_points, right_points), position|
|
pairings.each_with_index do |(left_index, right_index, left_points, right_points), position|
|
||||||
match = Match.create!(group: group, state: :finished, position: position, decider: false)
|
match = Match.create!(group:, state: :finished, position:, decider: false)
|
||||||
MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0)
|
MatchScore.create!(match:, team: teams[left_index], points: left_points, hidden_points: 0)
|
||||||
MatchScore.create!(match: match, team: teams[right_index], points: right_points, hidden_points: 0)
|
MatchScore.create!(match:, team: teams[right_index], points: right_points, hidden_points: 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_decider_match!(group:, teams:, hidden_points:)
|
def create_decider_match!(group:, teams:, hidden_points:)
|
||||||
match = Match.create!(group: group, state: :finished, position: 6, decider: true)
|
match = Match.create!(group:, state: :finished, position: 6, decider: true)
|
||||||
MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first)
|
MatchScore.create!(match:, team: teams.first, points: 0, hidden_points: hidden_points.first)
|
||||||
MatchScore.create!(match: match, team: teams.second, points: 0, hidden_points: hidden_points.last)
|
MatchScore.create!(match:, team: teams.second, points: 0, hidden_points: hidden_points.last)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_group_scores!(group:, teams:)
|
def create_group_scores!(group:, teams:)
|
||||||
teams.each_with_index do |team, index|
|
teams.each_with_index do |team, index|
|
||||||
GroupScore.create!(
|
GroupScore.create!(
|
||||||
group: group,
|
group:,
|
||||||
team: team,
|
team:,
|
||||||
group_points: 9 - index,
|
group_points: 9 - index,
|
||||||
scored_points: 12 - index,
|
scored_points: 12 - index,
|
||||||
received_points: index,
|
received_points: index,
|
||||||
|
|
@ -185,7 +187,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||||
timestamp: tournament.timestamp&.iso8601,
|
timestamp: tournament.timestamp&.iso8601,
|
||||||
timer_mode: tournament.timer_mode,
|
timer_mode: tournament.timer_mode,
|
||||||
teams: tournament.teams.order(:name).map { |team| team.name },
|
teams: tournament.teams.order(:name).map(&:name),
|
||||||
team_action_lists: tournament.team_action_lists.order(:key).map do |list|
|
team_action_lists: tournament.team_action_lists.order(:key).map do |list|
|
||||||
{
|
{
|
||||||
key: list.key,
|
key: list.key,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ RSpec.describe TournamentSyncEnqueue do
|
||||||
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
||||||
sync_auth_token: 'shared-secret')
|
sync_auth_token: 'shared-secret')
|
||||||
entry = TournamentSyncQueueEntry.create!(
|
entry = TournamentSyncQueueEntry.create!(
|
||||||
tournament: tournament,
|
tournament:,
|
||||||
snapshot_json: '{"old":true}',
|
snapshot_json: '{"old":true}',
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
attempts: 4,
|
attempts: 4,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ RSpec.describe TournamentSyncProcessor do
|
||||||
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
sync_target_url: 'https://remote.example.com/tournaments/1/sync_state',
|
||||||
sync_auth_token: 'shared-secret')
|
sync_auth_token: 'shared-secret')
|
||||||
TournamentSyncEnqueue.call(tournament)
|
TournamentSyncEnqueue.call(tournament)
|
||||||
entry = TournamentSyncQueueEntry.find_by!(tournament: tournament)
|
entry = TournamentSyncQueueEntry.find_by!(tournament:)
|
||||||
|
|
||||||
allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed, 'network down')
|
allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed,
|
||||||
|
'network down')
|
||||||
|
|
||||||
described_class.process_due!
|
described_class.process_due!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ module PerformanceHelpers
|
||||||
end
|
end
|
||||||
|
|
||||||
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
||||||
{ result: result, query_count: query_count, duration_ms: duration_ms }
|
{ result:, query_count:, duration_ms: }
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_group_stage_tournament(groups_count:, teams_per_group:)
|
def create_group_stage_tournament(groups_count:, teams_per_group:)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'ruby-prof'
|
require 'ruby-prof'
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
|
|
||||||
2
tasks.py
2
tasks.py
|
|
@ -670,7 +670,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
|
||||||
@task
|
@task
|
||||||
def lint(c):
|
def lint(c):
|
||||||
"""Run RuboCop."""
|
"""Run RuboCop."""
|
||||||
c.run(_bundle_exec("rubocop"), pty=True)
|
c.run(_bundle_exec("rubocop", "--cache", "false"), pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task(name="bootstrap-dev-user")
|
@task(name="bootstrap-dev-user")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue