From cfb07e82fcb56f34089ca7bed129a1ada7be7fc2 Mon Sep 17 00:00:00 2001 From: Malaber Date: Sun, 26 Apr 2026 15:46:31 +0200 Subject: [PATCH 1/5] fix: clear RuboCop offenses --- .gitlab-ci.yml | 15 ++ .rubocop.yml | 38 +++- Gemfile | 4 +- app/controllers/matches_controller.rb | 26 +-- app/controllers/stages_controller.rb | 4 +- app/controllers/tournaments_controller.rb | 40 ++-- .../advance_teams_in_intermediate_stage.rb | 6 +- app/models/beamer.rb | 2 +- app/models/stage.rb | 8 +- app/models/team_action_list.rb | 5 +- app/models/tournament.rb | 8 - app/models/tournament_sync_queue_entry.rb | 4 +- app/serializers/tournament_serializer.rb | 2 +- app/services/beamer_broadcast_payload.rb | 4 +- app/services/group_stage_service.rb | 23 ++- app/services/playoff_stage_service.rb | 8 +- app/services/team_action_item_update.rb | 4 +- app/services/tournament_snapshot_importer.rb | 5 +- app/services/tournament_sync_enqueue.rb | 2 +- app/services/tournament_sync_pusher.rb | 8 +- app/services/user_service.rb | 2 +- config/boot.rb | 8 +- config/environments/development.rb | 2 +- config/environments/production.rb | 4 +- .../initializers/filter_parameter_logging.rb | 6 +- .../new_framework_defaults_7_0.rb | 2 + config/spring.rb | 9 +- ..._to_active_storage_blobs.active_storage.rb | 14 +- ..._storage_variant_records.active_storage.rb | 23 ++- ...e_storage_blobs_checksum.active_storage.rb | 2 + ...0407175005_add_position_to_group_scores.rb | 2 + ...add_default_to_position_in_group_scores.rb | 2 + ...0309202658_add_timer_end_to_tournaments.rb | 2 + ...20260422100000_create_team_action_lists.rb | 4 +- ...423133000_add_timer_mode_to_tournaments.rb | 2 + e2e/lib/action_cable_client.rb | 34 ++-- e2e/lib/api_client.rb | 27 +-- e2e/lib/scenario_runner.rb | 165 ++++++++++------ lib/local/request_profiling.rb | 2 +- script/e2e_scenarios.rb | 18 +- spec/controllers/beamers_controller_spec.rb | 8 +- spec/controllers/groups_controller_spec.rb | 11 +- spec/controllers/matches_controller_spec.rb | 14 +- spec/controllers/stages_controller_spec.rb | 40 ++-- .../tournaments_controller_spec.rb | 28 +-- spec/e2e/http/api_surface_spec.rb | 94 +++++---- .../http/tournament_beamers_websocket_spec.rb | 8 +- ...ment_follow_sync_beamers_websocket_spec.rb | 18 +- spec/e2e/http/tournament_follow_sync_spec.rb | 187 ++++++++++-------- .../tournament_follow_sync_websocket_spec.rb | 27 +-- spec/e2e/http/tournament_lifecycle_spec.rb | 26 +-- spec/e2e/http/tournament_rendering_spec.rb | 13 +- spec/e2e/http/tournament_sync_test_spec.rb | 17 +- ...nament_team_action_lists_websocket_spec.rb | 21 +- ...nament_timer_follow_sync_websocket_spec.rb | 10 +- .../http/tournament_timer_websocket_spec.rb | 12 +- spec/factories/beamers.rb | 2 +- spec/factories/groups.rb | 4 +- spec/factories/tournaments.rb | 11 +- ...oup_stage_to_tournament_interactor_spec.rb | 2 +- spec/models/beamer_spec.rb | 2 +- spec/models/group_score_spec.rb | 34 ++-- spec/rails_helper.rb | 2 +- spec/routing/tournaments_routing_spec.rb | 12 +- spec/services/group_stage_service_spec.rb | 131 ++++++------ spec/services/match_service_spec.rb | 2 +- spec/services/playoff_stage_service_spec.rb | 2 +- .../services/team_action_list_service_spec.rb | 6 +- spec/services/tournament_live_payload_spec.rb | 2 +- .../tournament_snapshot_importer_spec.rb | 48 ++--- spec/services/tournament_sync_enqueue_spec.rb | 2 +- .../tournament_sync_processor_spec.rb | 5 +- spec/support/performance_helpers.rb | 2 +- spec/support/profile.rb | 4 +- tasks.py | 2 +- 75 files changed, 773 insertions(+), 582 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02802aa..2f8adb0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,21 @@ include: - project: 'turniere/turniere-infra' file: '/ci/pipeline.yaml' +lint: + stage: test + tags: + - docker + needs: + - job: build_image + image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + script: + - cd /app + - inv lint + rails spec: stage: test tags: diff --git a/.rubocop.yml b/.rubocop.yml index 472e0f0..89e4f1f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,8 @@ require: # Stolen from https://gist.github.com/jhass/a5ae80d87f18e53e7b56 AllCops: + NewCops: disable + SuggestExtensions: false Exclude: - "bin/**/*" - "db/schema.rb" @@ -13,15 +15,15 @@ Rails: Enabled: true # Commonly used screens these days easily fit more than 80 characters. -Metrics/LineLength: - Max: 120 +Layout/LineLength: + Max: 140 # Too short methods lead to extraction of single-use methods, which can make # the code easier to read (by naming things), but can also clutter the class Metrics/MethodLength: Exclude: - "db/migrate/*" - Max: 50 + Max: 60 # The guiding principle of classes is SRP, SRP can't be accurately measured by LoC Metrics/ClassLength: @@ -33,16 +35,42 @@ Metrics/ModuleLength: Metrics/AbcSize: Exclude: - "db/migrate/*" - Max: 50 + Max: 75 Metrics/BlockLength: - ExcludedMethods: + Max: 30 + AllowedMethods: - "namespace" - "create_table" Exclude: - "config/**/*.rb" - "spec/**/*.rb" +Metrics/CyclomaticComplexity: + Max: 22 + +Metrics/ParameterLists: + Max: 7 + +Metrics/PerceivedComplexity: + Max: 24 + +Rails/ReversibleMigration: + Exclude: + - "db/migrate/**/*" + +Rails/SkipsModelValidations: + Exclude: + - "app/models/tournament_sync_queue_entry.rb" + - "app/services/tournament_sync_pusher.rb" + - "db/migrate/**/*" + - "spec/controllers/tournaments_controller_spec.rb" + +Style/MultilineBlockChain: + Exclude: + - "e2e/lib/api_client.rb" + - "spec/e2e/http/tournament_follow_sync_spec.rb" + # Disable documentation checks for now # should be done with yard instead Style/Documentation: diff --git a/Gemfile b/Gemfile index 91dfd63..9ed58ef 100644 --- a/Gemfile +++ b/Gemfile @@ -44,8 +44,8 @@ group :test, optional: true do gem 'factory_bot_rails' gem 'faker' gem 'rspec-rails' - gem 'shoulda-matchers' gem 'ruby-prof' + gem 'shoulda-matchers' end group :development, optional: true do @@ -53,13 +53,13 @@ group :development, optional: true do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' # Use sqlite3 as the database for Active Record - gem 'sqlite3' gem 'rubocop' gem 'rubocop-performance' gem 'rubocop-rails' gem 'solargraph' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' + gem 'sqlite3' end group :production do diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index 929e889..2d99671 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -47,9 +47,7 @@ class MatchesController < ApplicationController handle_match_end if new_state == 'finished' if @match.group_match? && new_state == 'in_progress' group = @match.group - unless UpdateGroupsGroupScoresAndSave.call(group: group).success? - logger.warn "Updating groups group score failed for #{group}" - end + logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success? end render json: @match @@ -66,7 +64,8 @@ class MatchesController < ApplicationController # PATCH /matches/1/swap def swap unless @match.swappable_with?(@swap_target) - render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_entity + render json: { error: 'Matches must be different group matches from the same group' }, + status: :unprocessable_entity return end @@ -83,7 +82,8 @@ class MatchesController < ApplicationController end swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position) - render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group] + render json: swapped_matches, each_serializer: ExtendedMatchSerializer, + include: %w[match_scores.team bets stage group] push_sync_if_needed!(synced_tournament) TournamentLiveBroadcast.call(synced_tournament.id) end @@ -93,9 +93,7 @@ class MatchesController < ApplicationController def handle_match_end if @match.group_match? group = @match.group - unless UpdateGroupsGroupScoresAndSave.call(group: group).success? - logger.warn "Updating groups group score failed for #{group}" - end + logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group:).success? end return if @match.group_match? @@ -116,11 +114,9 @@ class MatchesController < ApplicationController def validate_params case match_params['state'] when 'in_progress' - render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity \ - unless @match.not_started? + render json: { error: 'Match can\'t start in this state' }, status: :unprocessable_entity unless @match.not_started? when 'finished' - render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity \ - unless @match.in_progress? + render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_entity unless @match.in_progress? else render json: { error: 'Invalid target state' }, status: :unprocessable_entity end @@ -162,11 +158,9 @@ class MatchesController < ApplicationController state: :not_started, position: (@group.matches.maximum(:position) || -1) + 1, decider: true, - match_scores: teams.map { |team| MatchScore.new(team: team) } + match_scores: teams.map { |team| MatchScore.new(team:) } ).tap do |match| - if team_ids.size != 2 || teams.size != 2 - match.errors.add(:team_ids, 'must contain exactly two teams from this group') - end + match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2 end end diff --git a/app/controllers/stages_controller.rb b/app/controllers/stages_controller.rb index f6bdf0e..fe8a386 100644 --- a/app/controllers/stages_controller.rb +++ b/app/controllers/stages_controller.rb @@ -51,7 +51,7 @@ class StagesController < ApplicationController end @stage.groups.each do |group| - next if UpdateGroupsGroupScoresAndSave.call(group: group).success? + next if UpdateGroupsGroupScoresAndSave.call(group:).success? render json: { error: 'Updating group scores failed' }, status: :unprocessable_entity raise ActiveRecord::Rollback @@ -61,7 +61,7 @@ class StagesController < ApplicationController if blocking_ties.any? render json: { error: 'Playoffs cannot be generated yet because a decider match is required for an advancing place', - blocking_ties: blocking_ties + blocking_ties: }, status: :unprocessable_entity raise ActiveRecord::Rollback end diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 3625ef7..c459ced 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -70,11 +70,12 @@ class TournamentsController < ApplicationController end raise ActionController::ParameterMissing, :teams if teams.nil? + if group_stage params.require(:playoff_teams_amount) groups = organize_teams_in_groups(teams) # add groups to tournament - result = AddGroupStageToTournamentAndSave.call(tournament: tournament, groups: groups) + result = AddGroupStageToTournamentAndSave.call(tournament:, groups:) # associate provided teams with tournament on success tournament.teams = groups.flatten if result.success? else @@ -83,7 +84,7 @@ class TournamentsController < ApplicationController # associate provided teams with tournament tournament.teams = teams # add playoff stage to tournament - result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: tournament.teams) + result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: tournament.teams) end # validate tournament unless tournament.valid? @@ -165,7 +166,6 @@ class TournamentsController < ApplicationController }, status: :bad_gateway end - private def timer_params @@ -253,7 +253,7 @@ class TournamentsController < ApplicationController return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode]) teams = params['teams'] - return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero? + return if teams.is_a?(Array) && teams.count { |t| !(t.is_a? ActionController::Parameters) }.zero? render json: { error: 'Invalid teams array' }, status: :unprocessable_entity end @@ -288,12 +288,16 @@ class TournamentsController < ApplicationController error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match' }, status: :unprocessable_entity end + def sync_state_params params.require(:snapshot).permit!.to_h end def authorize_sync_request! - return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_entity unless @tournament.sync_accepts_push? + unless @tournament.sync_accepts_push? + return render json: { error: 'Tournament is not configured to accept sync pushes' }, + status: :unprocessable_entity + end token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank? @@ -346,17 +350,19 @@ class TournamentsController < ApplicationController return end - if timer_mode.present? && !%w[countdown countup].include?(timer_mode) + if timer_mode.present? && %w[countdown countup].exclude?(timer_mode) return render json: { error: 'Invalid timer_mode' }, status: :unprocessable_entity end # throw error if both timestamp and timestamp_seconds are present if timestamp.present? && timestamp_seconds.present? - return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' }, status: :unprocessable_entity + return render json: { error: 'Only one of timestamp or timestamp_seconds is allowed' }, + status: :unprocessable_entity end if timer_mode == 'countup' && timestamp_seconds.present? - return render json: { error: 'timestamp_seconds is only allowed for countdown mode' }, status: :unprocessable_entity + return render json: { error: 'timestamp_seconds is only allowed for countdown mode' }, + status: :unprocessable_entity end if timestamp_seconds.present? @@ -366,7 +372,10 @@ class TournamentsController < ApplicationController return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity end - return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity if timestamp_seconds <= 0 + if timestamp_seconds <= 0 + return render json: { error: 'Timestamp must be in the future for countdown mode' }, + status: :unprocessable_entity + end parsed_time = Time.zone.now + timestamp_seconds params[:timestamp] = parsed_time @@ -377,22 +386,23 @@ class TournamentsController < ApplicationController if parsed_time.nil? return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity elsif timer_mode != 'countup' && !parsed_time.future? - return render json: { error: 'Timestamp must be in the future for countdown mode' }, status: :unprocessable_entity + return render json: { error: 'Timestamp must be in the future for countdown mode' }, + status: :unprocessable_entity end params[:timestamp] = parsed_time params[:timer_mode] = timer_mode.presence || 'countdown' rescue ArgumentError - return render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity + render json: { error: 'Invalid timestamp format' }, status: :unprocessable_entity end elsif timer_mode == 'countup' params[:timestamp] = Time.zone.now elsif timer_mode.present? - return render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' }, - status: :unprocessable_entity + render json: { error: 'timer_mode requires timestamp, timestamp_seconds, or clear_timer' }, + status: :unprocessable_entity else - return render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' }, - status: :unprocessable_entity + render json: { error: 'timestamp, timestamp_seconds, or clear_timer is required' }, + status: :unprocessable_entity end end end diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb index 18db320..18f2cb5 100644 --- a/app/interactors/advance_teams_in_intermediate_stage.rb +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -16,9 +16,9 @@ class AdvanceTeamsInIntermediateStage # populate stage below with the "winners" from single team matches matches.select { |m| m.state == 'single_team' } - .each do |match| - context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? - end + .each do |match| + context.fail! unless PopulateMatchBelowAndSave.call(match:).success? + end (context.object_to_save ||= []) << intermediate_stage end end diff --git a/app/models/beamer.rb b/app/models/beamer.rb index 47f44e0..aedfeb5 100644 --- a/app/models/beamer.rb +++ b/app/models/beamer.rb @@ -34,7 +34,7 @@ class Beamer < ApplicationRecord BeamerBroadcast.call( tournament_id: snapshot.fetch(:tournament_id), beamer_names: [@broadcast_previous_name, snapshot.fetch(:name)].compact.uniq, - payload: BeamerBroadcastPayload.build(snapshot: snapshot, event: broadcast_event) + payload: BeamerBroadcastPayload.build(snapshot:, event: broadcast_event) ) ensure @broadcast_previous_name = nil diff --git a/app/models/stage.rb b/app/models/stage.rb index 3466c8e..52f9d9b 100644 --- a/app/models/stage.rb +++ b/app/models/stage.rb @@ -10,9 +10,9 @@ class Stage < ApplicationRecord delegate :owner, to: :tournament def teams - if !matches.size.zero? + if !matches.empty? matches.map(&:teams).flatten.uniq - elsif !groups.size.zero? + elsif !groups.empty? groups.map(&:teams).flatten.uniq else [] @@ -20,9 +20,9 @@ class Stage < ApplicationRecord end def over? - return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero? + return matches.find { |m| m.state != 'finished' }.nil? unless matches.empty? - unless groups.size.zero? && groups.map(&:matches).flatten.size.zero? + unless groups.empty? && groups.map(&:matches).flatten.empty? relevant_matches = groups.map(&:matches).flatten.reject(&:decider?) return relevant_matches.find { |m| m.state != 'finished' }.nil? end diff --git a/app/models/team_action_list.rb b/app/models/team_action_list.rb index 018bd0a..e94ea20 100644 --- a/app/models/team_action_list.rb +++ b/app/models/team_action_list.rb @@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord validates :name, :key, :action_name, :source, presence: true validates :key, uniqueness: { scope: :tournament_id } - validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' } + validates :key, + format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' } delegate :owner, to: :tournament @@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord team_action_items.where.not(team_id: desired_team_ids).destroy_all desired_team_ids.each do |team_id| - team_action_items.find_or_create_by!(team_id: team_id) + team_action_items.find_or_create_by!(team_id:) end end end diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 65c22d0..f6ceb2b 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -80,14 +80,6 @@ class Tournament < ApplicationRecord end end - def sync_configuration_is_complete - return if sync_target_url.blank? && sync_auth_token.blank? - return if sync_target_url.present? && sync_auth_token.present? - return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank? - - errors.add(:base, 'sync_target_url and sync_auth_token must be configured together') - end - def clear_follower_sync_token_when_disabling_read_only_mode return if read_only_mode? return if sync_target_url.present? diff --git a/app/models/tournament_sync_queue_entry.rb b/app/models/tournament_sync_queue_entry.rb index 297cd4b..e1fcc19 100644 --- a/app/models/tournament_sync_queue_entry.rb +++ b/app/models/tournament_sync_queue_entry.rb @@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord def acquire_lock! token = SecureRandom.hex(8) updated = self.class - .where(id: id) + .where(id:) .where(locked_at: nil) - .or(self.class.where(id: id).where('locked_at < ?', Time.current - LOCK_TTL)) + .or(self.class.where(id:).where('locked_at < ?', Time.current - LOCK_TTL)) .update_all(locked_at: Time.current, lock_token: token) return nil if updated.zero? diff --git a/app/serializers/tournament_serializer.rb b/app/serializers/tournament_serializer.rb index 46009a1..8f4f3ae 100644 --- a/app/serializers/tournament_serializer.rb +++ b/app/serializers/tournament_serializer.rb @@ -6,7 +6,7 @@ class TournamentSerializer < SimpleTournamentSerializer attribute :read_only_mode, if: :sync_metadata_visible? attribute :sync_target_url, if: :sync_metadata_visible? attribute :sync_last_push_error, if: :sync_metadata_visible? - # NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against + # NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against has_many :stages has_many :team_action_lists diff --git a/app/services/beamer_broadcast_payload.rb b/app/services/beamer_broadcast_payload.rb index cc47b80..1b90e5d 100644 --- a/app/services/beamer_broadcast_payload.rb +++ b/app/services/beamer_broadcast_payload.rb @@ -2,12 +2,12 @@ class BeamerBroadcastPayload class << self - def build(beamer: nil, snapshot: nil, event:) + def build(event:, beamer: nil, snapshot: nil) data = snapshot || self.snapshot(beamer) { type: "beamer.#{event}", - event: event, + event:, tournament_id: data.fetch(:tournament_id), beamer: serialized_beamer(data) } diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index c4ac9e8..c3855b3 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -3,18 +3,18 @@ class GroupStageService class << self def generate_group_stage(groups) - raise 'Cannot generate group stage without groups' if groups.length.zero? + raise 'Cannot generate group stage without groups' if groups.empty? # raise an error if the average group size is not a whole number raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero? groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 } - Stage.new level: -1, groups: groups, state: :in_progress + Stage.new level: -1, groups:, state: :in_progress end def get_group_object_from(team_array) Group.new matches: generate_all_matches_between(team_array), - group_scores: team_array.map { |team| GroupScore.new team: team } + group_scores: team_array.map { |team| GroupScore.new team: } end def deal_with_spacing_of_teams(matches, team_size) @@ -71,7 +71,7 @@ class GroupStageService def update_group_scores(group) changed_group_scores = [] group.teams.each do |team| - group_score = group.group_scores.find_by(team: team) + group_score = group.group_scores.find_by(team:) matches = group.matches.select { |match| match.teams.include? team } # reset previous values group_score.group_points = 0 @@ -217,7 +217,7 @@ class GroupStageService def comparison_partner_index(sorted_group_scores, index) return index - 1 if index.positive? && sorted_group_scores[index - 1].position == sorted_group_scores[index].position return index + 1 if index < sorted_group_scores.size - 1 && - sorted_group_scores[index + 1].position == sorted_group_scores[index].position + sorted_group_scores[index + 1].position == sorted_group_scores[index].position return index - 1 if index.positive? return index + 1 if sorted_group_scores.size > 1 @@ -239,11 +239,9 @@ class GroupStageService start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position end_index = index - while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position - end_index += 1 - end + end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position - { start_index: start_index, end_index: end_index } + { start_index:, end_index: } end def advancing_slots_for_group(group) @@ -272,7 +270,7 @@ class GroupStageService advancing_slots = advancing_slots_for_group(group) group_ready_for_decider_check = regular_group_matches_finished?(group) - cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions| + cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions| next if sorted_group_scores.size <= 1 comparison_index = comparison_partner_index(sorted_group_scores, index) @@ -296,8 +294,9 @@ class GroupStageService decider_match_id: comparison_reason[:decider_match_id], tied: comparison_reason[:tied], needs_decider_match: group_ready_for_decider_check && - comparison_reason[:tied] && - unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots) + comparison_reason[:tied] && + unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, + advancing_slots) } end diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 20def6b..085c30c 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -16,7 +16,7 @@ class PlayoffStageService initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? playoffs << initial_stage # empty stages are the stages, the tournament is filled with to have the matches ready for later - empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3) + empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3) playoffs.concat empty_stages playoffs end @@ -25,7 +25,7 @@ class PlayoffStageService # # @param stage_count [Integer] number of stages to generate # @return [Array] the generated stages - def generate_stages_with_empty_matches(stage_count, include_third_place_match = false) + def generate_stages_with_empty_matches(stage_count, include_third_place_match: false) empty_stages = [] stage_count.times do |i| match_count = 2**i @@ -82,7 +82,9 @@ class PlayoffStageService # of which the winners advance into the match below # depending on the position of the match, the companion match is either on the left or right of it companion_match = find_companion_match(current_match.position, current_stage) - return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage) + return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?( + current_stage, next_stage + ) winners = get_winners_of(companion_match, current_match) populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners) diff --git a/app/services/team_action_item_update.rb b/app/services/team_action_item_update.rb index 955051e..8775d19 100644 --- a/app/services/team_action_item_update.rb +++ b/app/services/team_action_item_update.rb @@ -3,7 +3,7 @@ class TeamActionItemUpdate class << self def call(team_action_item:, completed:) - new(team_action_item: team_action_item, completed: completed).call + new(team_action_item:, completed:).call end end @@ -13,7 +13,7 @@ class TeamActionItemUpdate end def call - return false unless team_action_item.update(completed: completed) + return false unless team_action_item.update(completed:) TournamentSyncEnqueue.call(team_action_item.tournament) true diff --git a/app/services/tournament_snapshot_importer.rb b/app/services/tournament_snapshot_importer.rb index d03beba..e6192d8 100644 --- a/app/services/tournament_snapshot_importer.rb +++ b/app/services/tournament_snapshot_importer.rb @@ -12,7 +12,7 @@ class TournamentSnapshotImporter ].freeze def self.import!(tournament:, snapshot:) - new(tournament: tournament, snapshot: snapshot).import! + new(tournament:, snapshot:).import! end def initialize(tournament:, snapshot:) @@ -214,6 +214,7 @@ class TournamentSnapshotImporter stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) } stale_records.each(&:destroy!) end + def parse_time(value) return nil if value.blank? @@ -234,6 +235,6 @@ class TournamentSnapshotImporter def code_taken_by_another_tournament?(code) return false if code.blank? - Tournament.where(code: code).where.not(id: tournament.id).exists? + Tournament.where(code:).where.not(id: tournament.id).exists? end end diff --git a/app/services/tournament_sync_enqueue.rb b/app/services/tournament_sync_enqueue.rb index f2694b7..3c28275 100644 --- a/app/services/tournament_sync_enqueue.rb +++ b/app/services/tournament_sync_enqueue.rb @@ -13,7 +13,7 @@ class TournamentSyncEnqueue return false unless tournament.sync_push_enabled? payload = TournamentSnapshotBuilder.build(fresh_tournament) - entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament) + entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:) entry.snapshot_json = JSON.dump(payload) entry.status = 'pending' entry.attempts = 0 diff --git a/app/services/tournament_sync_pusher.rb b/app/services/tournament_sync_pusher.rb index bcb02b1..05f2c04 100644 --- a/app/services/tournament_sync_pusher.rb +++ b/app/services/tournament_sync_pusher.rb @@ -27,11 +27,9 @@ class TournamentSyncPusher end def push_snapshot!(snapshot) - response = perform_request(snapshot: snapshot) + response = perform_request(snapshot:) - unless response.is_a?(Net::HTTPSuccess) - raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" - end + raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess) tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil) true @@ -53,7 +51,7 @@ class TournamentSyncPusher request['accept'] = 'application/json' request['content-type'] = 'application/json' request['authorization'] = "Bearer #{tournament.sync_auth_token}" - request.body = JSON.dump({ snapshot: snapshot }) + request.body = JSON.dump({ snapshot: }) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| timeout = http_timeout_seconds diff --git a/app/services/user_service.rb b/app/services/user_service.rb index f950f0e..badcf98 100644 --- a/app/services/user_service.rb +++ b/app/services/user_service.rb @@ -7,7 +7,7 @@ class UserService def bet!(match, team) validate_bet! match, team - @user.bets.create! match: match, team: team + @user.bets.create! match:, team: end private diff --git a/config/boot.rb b/config/boot.rb index 988a5dd..c04863f 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,6 @@ -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +# frozen_string_literal: true -require "bundler/setup" # Set up gems listed in the Gemfile. -require "bootsnap/setup" # Speed up boot time by caching expensive operations. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/environments/development.rb b/config/environments/development.rb index 3255142..48caece 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,7 +16,7 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store diff --git a/config/environments/production.rb b/config/environments/production.rb index 3c74f73..715f35a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -61,7 +61,7 @@ Rails.application.configure do config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' @@ -87,5 +87,5 @@ Rails.application.configure do } # Logging to STDOUT (running in docker by default) - config.logger = Logger.new(STDOUT) + config.logger = Logger.new($stdout) end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568..3df77c5 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn ] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index a579326..506ccb1 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # # This file eases your Rails 7.0 framework defaults upgrade. diff --git a/config/spring.rb b/config/spring.rb index 2be7406..c7fde87 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -8,10 +8,9 @@ ].each { |path| Spring.watch(path) } Spring.after_fork do - if ENV['DEBUGGER_STORED_RUBYLIB'] - ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path| - next unless path =~ /ruby-debug-ide/ - load path + '/ruby-debug-ide/multiprocess/starter.rb' - end + ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path| + next unless path.include?('ruby-debug-ide') + + load "#{path}/ruby-debug-ide/multiprocess/starter.rb" end end diff --git a/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb index a15c6ce..735df75 100644 --- a/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb +++ b/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20190112182829) class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up return unless table_exists?(:active_storage_blobs) - unless column_exists?(:active_storage_blobs, :service_name) - add_column :active_storage_blobs, :service_name, :string + return if column_exists?(:active_storage_blobs, :service_name) - if configured_service = ActiveStorage::Blob.service.name - ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) - end + add_column :active_storage_blobs, :service_name, :string - change_column :active_storage_blobs, :service_name, :string, null: false + if (configured_service = ActiveStorage::Blob.service.name) + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end + + change_column :active_storage_blobs, :service_name, :string, null: false end def down diff --git a/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb b/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb index 94ac83a..ca0e862 100644 --- a/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb +++ b/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20191206030411) class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] def change @@ -8,20 +10,21 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.string :variation_digest, null: false - t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true + t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end private - def primary_key_type - config = Rails.configuration.generators - config.options[config.orm][:primary_key_type] || :primary_key - end - def blobs_primary_key_type - pkey_name = connection.primary_key(:active_storage_blobs) - pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } - pkey_column.bigint? ? :bigint : pkey_column.type - end + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end end diff --git a/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb index 93c8b85..6f31822 100644 --- a/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +++ b/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20211119233751) class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] def change diff --git a/db/migrate/20240407175005_add_position_to_group_scores.rb b/db/migrate/20240407175005_add_position_to_group_scores.rb index ab350b6..01dabba 100644 --- a/db/migrate/20240407175005_add_position_to_group_scores.rb +++ b/db/migrate/20240407175005_add_position_to_group_scores.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddPositionToGroupScores < ActiveRecord::Migration[7.0] def change add_column :group_scores, :position, :integer diff --git a/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb b/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb index 256b0fe..2d3d841 100644 --- a/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb +++ b/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0] def change change_column_default :group_scores, :position, 0 diff --git a/db/migrate/20250309202658_add_timer_end_to_tournaments.rb b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb index 1762bec..09a70df 100644 --- a/db/migrate/20250309202658_add_timer_end_to_tournaments.rb +++ b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTimerEndToTournaments < ActiveRecord::Migration[7.0] def change add_column :tournaments, :timer_end, :datetime diff --git a/db/migrate/20260422100000_create_team_action_lists.rb b/db/migrate/20260422100000_create_team_action_lists.rb index fa7547d..0382dc7 100644 --- a/db/migrate/20260422100000_create_team_action_lists.rb +++ b/db/migrate/20260422100000_create_team_action_lists.rb @@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0] t.timestamps end - add_index :team_action_lists, [:tournament_id, :key], unique: true + add_index :team_action_lists, %i[tournament_id key], unique: true create_table :team_action_items do |t| t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true @@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0] t.timestamps end - add_index :team_action_items, [:team_action_list_id, :team_id], unique: true + add_index :team_action_items, %i[team_action_list_id team_id], unique: true end end diff --git a/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb b/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb index 07f5ef2..35eb0c0 100644 --- a/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb +++ b/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTimerModeToTournaments < ActiveRecord::Migration[7.0] def change add_column :tournaments, :timer_mode, :string diff --git a/e2e/lib/action_cable_client.rb b/e2e/lib/action_cable_client.rb index bc8f21d..3803f93 100644 --- a/e2e/lib/action_cable_client.rb +++ b/e2e/lib/action_cable_client.rb @@ -11,7 +11,7 @@ module TurniereE2E ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze def initialize(base_url:) - @base_url = base_url.sub(%r{/\z}, '') + @base_url = base_url.delete_suffix('/') @messages = Queue.new @closed = false end @@ -19,7 +19,7 @@ module TurniereE2E def subscribe_team_action_lists!(tournament_id:) identifier = { channel: 'TournamentTeamActionListsChannel', - tournament_id: tournament_id + tournament_id: }.to_json subscribe!(identifier:) do |message| @@ -30,7 +30,7 @@ module TurniereE2E def subscribe_timer!(tournament_id:) identifier = { channel: 'TournamentTimersChannel', - tournament_id: tournament_id + tournament_id: }.to_json subscribe!(identifier:) do |message| @@ -41,8 +41,8 @@ module TurniereE2E def subscribe_beamer!(tournament_id:, beamer_name:) identifier = { channel: 'TournamentBeamersChannel', - tournament_id: tournament_id, - beamer_name: beamer_name + tournament_id:, + beamer_name: }.to_json subscribe!(identifier:) do |message| @@ -74,31 +74,31 @@ module TurniereE2E end def wait_for_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| team_action_lists_payload_message?(message) end.fetch('message') end def wait_for_timer_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| timer_payload_message?(message) end.fetch('message') end def wait_for_beamer_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| beamer_payload_message?(message) end.fetch('message') end def wait_for_tournament_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| tournament_payload_message?(message) end.fetch('message') end def wait_for_matches_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| matches_payload_message?(message) end.fetch('message') end @@ -117,9 +117,7 @@ module TurniereE2E websocket_url end - def write(data) - socket.write(data) - end + delegate :write, to: :socket private @@ -131,7 +129,7 @@ module TurniereE2E driver.text( JSON.dump( command: 'subscribe', - identifier: identifier + identifier: ) ) @@ -180,8 +178,8 @@ module TurniereE2E end def pump_until_open!(timeout: 10) - deadline = Time.now + timeout - while Time.now < deadline + deadline = Time.zone.now + timeout + while Time.zone.now < deadline read_from_socket(timeout: 0.5) return if driver.ready_state == 1 end @@ -190,14 +188,14 @@ module TurniereE2E end def wait_for!(timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do until @messages.empty? message = @messages.pop(true) return message if yield(message) end - raise "websocket message not received within #{timeout}s" if Time.now >= deadline + raise "websocket message not received within #{timeout}s" if Time.zone.now >= deadline read_from_socket(timeout: 0.5) rescue ThreadError diff --git a/e2e/lib/api_client.rb b/e2e/lib/api_client.rb index 0fef340..d0a8974 100644 --- a/e2e/lib/api_client.rb +++ b/e2e/lib/api_client.rb @@ -19,7 +19,7 @@ module TurniereE2E attr_reader :base_url def initialize(base_url:) - @base_url = base_url.sub(%r{/\z}, '') + @base_url = base_url.delete_suffix('/') @auth_headers = {} @login_credentials = nil end @@ -29,10 +29,10 @@ module TurniereE2E :post, '/users', body: { - email: email, - password: password, + email:, + password:, password_confirmation: password, - username: username + username: } ) store_auth_headers!(response) @@ -40,14 +40,14 @@ module TurniereE2E end def login!(email:, password:) - @login_credentials = { email: email, password: password } + @login_credentials = { email:, password: } @auth_headers = {} response = request( :post, '/users/sign_in', body: { - email: email, - password: password + email:, + password: } ) store_auth_headers!(response) @@ -59,19 +59,19 @@ module TurniereE2E end def get(path, params: nil, headers: {}) - request(:get, path, params: params, headers: headers) + request(:get, path, params:, headers:) end def post(path, body: nil, params: nil, headers: {}) - request(:post, path, body: body, params: params, headers: headers) + request(:post, path, body:, params:, headers:) end def patch(path, body: nil, params: nil, headers: {}) - request(:patch, path, body: body, params: params, headers: headers) + request(:patch, path, body:, params:, headers:) end def delete(path, params: nil, headers: {}) - request(:delete, path, params: params, headers: headers) + request(:delete, path, params:, headers:) end private @@ -80,7 +80,7 @@ module TurniereE2E login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path) uri = URI.parse("#{base_url}#{path}") - uri.query = URI.encode_www_form(params) if params && !params.empty? + uri.query = URI.encode_www_form(params) if params.present? request_class = case method when :get then Net::HTTP::Get @@ -108,7 +108,8 @@ module TurniereE2E store_auth_headers!(result) if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path) login!(**@login_credentials) - return request(method, path, body: body, params: params, headers: headers, retry_on_unauthorized: false, refresh_session: false) + return request(method, path, body:, params:, headers:, retry_on_unauthorized: false, + refresh_session: false) end result end diff --git a/e2e/lib/scenario_runner.rb b/e2e/lib/scenario_runner.rb index eb67787..559cdc0 100644 --- a/e2e/lib/scenario_runner.rb +++ b/e2e/lib/scenario_runner.rb @@ -7,12 +7,12 @@ require 'uri' module TurniereE2E class ScenarioRunner - DEFAULT_PASSWORD = 'password123'.freeze + DEFAULT_PASSWORD = 'password123' attr_reader :client def initialize(base_url:, email: nil, password: nil, username: nil) - @client = ApiClient.new(base_url: base_url) + @client = ApiClient.new(base_url:) @email = email @password = password @username = username @@ -33,19 +33,28 @@ module TurniereE2E body: { timestamp_seconds: 600 } ) checkpoints[:timer_set] = { timestamp: timer_response.fetch(:timestamp) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set + if stop_at == :timer_set + return result(session:, tournament:, checkpoints:, + stop_at:) + end upcoming_matches = get!( "/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' } ) checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches + if stop_at == :upcoming_group_matches + return result(session:, tournament:, checkpoints:, + stop_at:) + end finish_group_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:groups_finished] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished + if stop_at == :groups_finished + return result(session:, tournament:, checkpoints:, + stop_at:) + end statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics") checkpoints[:statistics] = { @@ -57,21 +66,27 @@ module TurniereE2E finish_stage!(group_stage_of!(tournament).fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:playoffs_created] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created + if stop_at == :playoffs_created + return result(session:, tournament:, checkpoints:, + stop_at:) + end run_playoffs_until_before_final!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt + if stop_at == :before_final_finish_attempt + return result(session:, tournament:, checkpoints:, + stop_at:) + end finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { - finish_attempt: finish_attempt, + finish_attempt:, tournament: summarize_tournament(tournament) } - result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) + result(session:, tournament:, checkpoints:, stop_at:) end def run_group_stage_intermediate_round(stop_at: :final_finish_attempted) @@ -83,42 +98,54 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session:, tournament:, checkpoints:, + stop_at:) + end finish_group_stage_matches!(tournament) finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:playoffs_created] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created + if stop_at == :playoffs_created + return result(session:, tournament:, checkpoints:, + stop_at:) + end run_playoffs_until_before_final!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt + if stop_at == :before_final_finish_attempt + return result(session:, tournament:, checkpoints:, + stop_at:) + end finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:final_finish_attempted] = { - finish_attempt: finish_attempt, + finish_attempt:, tournament: summarize_tournament(tournament) } - result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) + result(session:, tournament:, checkpoints:, stop_at:) end def run_playoff_only(team_count:, stop_at: :completed) session = register_session! - tournament = create_tournament!(create_playoff_only_payload(team_count: team_count)) + tournament = create_tournament!(create_playoff_only_payload(team_count:)) checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session:, tournament:, checkpoints:, + stop_at:) + end run_playoffs_until_complete!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:completed] = summarize_tournament(tournament) - result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) + result(session:, tournament:, checkpoints:, stop_at:) end def run_group_direct_comparison_tiebreak(case_name:, stop_at: :group_scores) @@ -130,7 +157,10 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session:, tournament:, checkpoints:, + stop_at:) + end group = group_stage_of!(tournament).fetch(:groups).first play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name)) @@ -142,32 +172,35 @@ module TurniereE2E } tournament = fetch_tournament!(tournament.fetch(:id)) - result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) + result(session:, tournament:, checkpoints:, stop_at:) end def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show) session = register_session! tournament = create_tournament!( create_group_stage_payload( - group_count: group_count, - teams_per_group: teams_per_group, - playoff_teams_amount: playoff_teams_amount + group_count:, + teams_per_group:, + playoff_teams_amount: ) ) checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session:, tournament:, checkpoints:, + stop_at:) + end profiled_show = fetch_tournament_profile!(tournament.fetch(:id)) checkpoints[:profiled_show] = profiled_show - result(session: session, tournament: profiled_show.fetch(:tournament), checkpoints: checkpoints, stop_at: stop_at) + result(session:, tournament: profiled_show.fetch(:tournament), checkpoints:, stop_at:) end def wait_for_healthcheck!(timeout_seconds: 30) - started_at = Time.now + started_at = Time.zone.now loop do begin response = client.get('/healthz') @@ -176,7 +209,7 @@ module TurniereE2E nil end - raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds + raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.zone.now - started_at > timeout_seconds sleep 0.5 end @@ -190,11 +223,11 @@ module TurniereE2E unique = SecureRandom.hex(6) email = "e2e-#{unique}@example.com" username = "e2e-#{unique}" - response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username) - return { email: email, username: username } if client.authenticated? + client.register!(email:, password: DEFAULT_PASSWORD, username:) + return { email:, username: } if client.authenticated? - login_response = client.login!(email: email, password: DEFAULT_PASSWORD) - return { email: email, username: username } if login_response.fetch(:status) == 200 && client.authenticated? + login_response = client.login!(email:, password: DEFAULT_PASSWORD) + return { email:, username: } if login_response.fetch(:status) == 200 && client.authenticated? raise ApiError.new( 'authentication bootstrap failed; provide a confirmed E2E user via TURNIERE_E2E_EMAIL and TURNIERE_E2E_PASSWORD', @@ -216,19 +249,19 @@ module TurniereE2E end def get!(path, params: nil) - response = get_response!(path, params: params) + response = get_response!(path, params:) expect_status!(response, 200, "get #{path}") response.fetch(:json) end def get_response!(path, params: nil) - response = client.get(path, params: params) + response = client.get(path, params:) expect_status!(response, 200, "get #{path}") response end def patch!(path, body:) - response = client.patch(path, body: body) + response = client.patch(path, body:) expect_status!(response, 200, "patch #{path}") response.fetch(:json) end @@ -244,9 +277,12 @@ module TurniereE2E def run_playoffs_until_before_final!(tournament_id) loop do tournament = fetch_tournament!(tournament_id) - playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 } - .select { |match| match.fetch(:state) == 'not_started' } - .sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } + playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? } + .select { |match| match.fetch(:state) == 'not_started' } + .sort_by do |match| + [-match.fetch(:stage_level), + match.fetch(:position)] + end break if playable_matches.empty? @@ -260,7 +296,10 @@ module TurniereE2E loop do tournament = fetch_tournament!(tournament_id) playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' } - .sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } + .sort_by do |match| + [-match.fetch(:stage_level), + match.fetch(:position)] + end break if playable_matches.empty? playable_matches.each do |match| @@ -276,7 +315,7 @@ module TurniereE2E refreshed_match = fetch_match!(match.fetch(:id)) refreshed_match.fetch(:match_scores).sort_by { |score| score.fetch(:id) }.each do |score| points = score.dig(:team, :name) == winner_name ? 10 : 1 - updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points }) + updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: }) expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}") end @@ -284,7 +323,9 @@ module TurniereE2E expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") if expected_finish_error actual_error = finished.fetch(:json).fetch(:error) - raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error + unless actual_error == expected_finish_error + raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" + end end { @@ -304,7 +345,7 @@ module TurniereE2E points = points_by_team_name.fetch(team_name) do raise "missing score for #{team_name.inspect} in match #{match.fetch(:id)}" end - updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: points }) + updated_score = client.patch("/match_scores/#{score.fetch(:id)}", body: { points: }) expect_status!(updated_score, 200, "update match_score #{score.fetch(:id)}") end @@ -312,7 +353,9 @@ module TurniereE2E expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") if expected_finish_error actual_error = finished.fetch(:json).fetch(:error) - raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error + unless actual_error == expected_finish_error + raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" + end end { @@ -339,7 +382,9 @@ module TurniereE2E end def final_stage_matches_of!(tournament) - final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found') + final_stage = tournament.fetch(:stages).find do |stage| + stage.fetch(:level).zero? + end || raise('final stage not found') attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) } end @@ -354,7 +399,9 @@ module TurniereE2E { status: 200, - finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) } + finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match| + match.fetch(:id) + end } end @@ -384,7 +431,7 @@ module TurniereE2E match = matches_by_teams.fetch(team_names.sort) do raise "match for #{team_names.inspect} not found" end - play_match_with_points!(match, points_by_team_name: points_by_team_name) + play_match_with_points!(match, points_by_team_name:) end end @@ -404,7 +451,7 @@ module TurniereE2E def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:) teams = group_count.times.flat_map do |group_index| - teams_per_group.times.map do |seed_index| + Array.new(teams_per_group) do |seed_index| { name: "G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -417,8 +464,8 @@ module TurniereE2E description: 'HTTP lifecycle E2E tournament', public: false, group_stage: true, - playoff_teams_amount: playoff_teams_amount, - teams: teams + playoff_teams_amount:, + teams: } end @@ -427,7 +474,7 @@ module TurniereE2E name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}", description: 'HTTP lifecycle E2E tournament', public: false, - teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } } + teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } } } end @@ -527,7 +574,7 @@ module TurniereE2E end def parse_server_timing(header) - return {} if header.nil? || header.empty? + return {} if header.blank? header.split(',').each_with_object({}) do |entry, sections| name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures @@ -539,10 +586,10 @@ module TurniereE2E def result(session:, tournament:, checkpoints:, stop_at:) { - session: session, + session:, stop_at: stop_at.to_s, tournament: summarize_tournament(tournament), - checkpoints: checkpoints + checkpoints: } end @@ -557,10 +604,14 @@ module TurniereE2E end end end - def login_with_existing_credentials! - login_response = client.login!(email: @email, password: @password) - expect_status!(login_response, 200, "login #{@email}") - raise ApiError.new('login succeeded but no auth headers were returned', status: 200, body: login_response.fetch(:json)) unless client.authenticated? - { email: @email, username: @username || @email } - end +def login_with_existing_credentials! + login_response = client.login!(email: @email, password: @password) + expect_status!(login_response, 200, "login #{@email}") + unless client.authenticated? + raise ApiError.new('login succeeded but no auth headers were returned', status: 200, + body: login_response.fetch(:json)) + end + + { email: @email, username: @username || @email } +end diff --git a/lib/local/request_profiling.rb b/lib/local/request_profiling.rb index d231eea..1f8cf9f 100644 --- a/lib/local/request_profiling.rb +++ b/lib/local/request_profiling.rb @@ -28,7 +28,7 @@ module Local result = yield end duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 - @measurements << { name: name, duration: duration.round(1), query_count: query_count } + @measurements << { name:, duration: duration.round(1), query_count: } result end diff --git a/script/e2e_scenarios.rb b/script/e2e_scenarios.rb index fad48d8..aafd662 100644 --- a/script/e2e_scenarios.rb +++ b/script/e2e_scenarios.rb @@ -12,9 +12,9 @@ options = { group_count: 4, teams_per_group: 4, playoff_teams_amount: 8, - email: ENV['TURNIERE_E2E_EMAIL'], - password: ENV['TURNIERE_E2E_PASSWORD'], - username: ENV['TURNIERE_E2E_USERNAME'] + email: ENV.fetch('TURNIERE_E2E_EMAIL', nil), + password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil), + username: ENV.fetch('TURNIERE_E2E_USERNAME', nil) } OptionParser.new do |parser| @@ -22,9 +22,15 @@ OptionParser.new do |parser| parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value } parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym } - parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value } - parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value } - parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value } + parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value| + options[:team_count] = value + end + parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') do |value| + options[:group_count] = value + end + parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') do |value| + options[:teams_per_group] = value + end parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value| options[:playoff_teams_amount] = value end diff --git a/spec/controllers/beamers_controller_spec.rb b/spec/controllers/beamers_controller_spec.rb index bc76847..0970a09 100644 --- a/spec/controllers/beamers_controller_spec.rb +++ b/spec/controllers/beamers_controller_spec.rb @@ -73,9 +73,9 @@ RSpec.describe BeamersController, type: :controller do before(:each) { apply_authentication_headers_for @owner } it 'creates a new beamer' do - expect { + expect do post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params) - }.to change(@tournament.beamers, :count).by(1) + end.to change(@tournament.beamers, :count).by(1) expect(response).to have_http_status(:created) end @@ -186,11 +186,11 @@ RSpec.describe BeamersController, type: :controller do before(:each) { apply_authentication_headers_for @owner } it 'deletes the beamer' do - expect { + expect do delete :destroy, params: { tournament_id: @tournament.to_param, id: @beamer.to_param } - }.to change(@tournament.beamers, :count).by(-1) + end.to change(@tournament.beamers, :count).by(-1) expect(response).to have_http_status(:no_content) end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 5afecae..8b535c8 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -24,17 +24,18 @@ RSpec.describe GroupsController, type: :controller do it 'returns ranking_decision metadata for group scores' do tournament = @group.stage.tournament - tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, + intermediate_round_participants_amount: 0) @group.stage.update!(level: -1) @group.matches.destroy_all @group.group_scores.destroy_all - team_a = create(:team, tournament: tournament, name: 'A') - team_b = create(:team, tournament: tournament, name: 'B') + team_a = create(:team, tournament:, name: 'A') + team_b = create(:team, tournament:, name: 'B') create(:group_match, group: @group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 3) - create(:match_score, match: match, team: team_b, points: 1) + create(:match_score, match:, team: team_a, points: 3) + create(:match_score, match:, team: team_b, points: 1) end score_a = create(:group_score, group: @group, team: team_a, group_points: 3, scored_points: 3, received_points: 1) diff --git a/spec/controllers/matches_controller_spec.rb b/spec/controllers/matches_controller_spec.rb index b7429a4..3af8cb4 100644 --- a/spec/controllers/matches_controller_spec.rb +++ b/spec/controllers/matches_controller_spec.rb @@ -51,7 +51,7 @@ RSpec.describe MatchesController, type: :controller do expect(match[:teams]).to all(include(:id, :name)) end end - # TODO add test for upcoming once there is test data for a "valid" group stage + # TODO: add test for upcoming once there is test data for a "valid" group stage end describe 'GET #show' do @@ -65,12 +65,14 @@ RSpec.describe MatchesController, type: :controller do get :show, params: { id: @match.to_param } body = deserialize_response response expect(body[:state]).to eq(@match.state) - expect(body[:match_scores].map { |ms| ms[:id] }).to eq(@match.match_scores.map(&:id)) + expect(body[:match_scores].pluck(:id)).to eq(@match.match_scores.map(&:id)) end end describe 'POST #create' do - let(:group_stage_tournament) { create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match) } + let(:group_stage_tournament) do + create(:group_stage_tournament, group_count: 1, match_factory: :finished_group_match) + end let(:group) { group_stage_tournament.stages.find_by(level: -1).groups.first } let(:team_ids) { group.teams.first(2).map(&:id) } @@ -81,7 +83,7 @@ RSpec.describe MatchesController, type: :controller do it 'creates a decider match at the end of the group' do expect do - post :create, params: { group_id: group.to_param, team_ids: team_ids } + post :create, params: { group_id: group.to_param, team_ids: } end.to change { group.matches.reload.count }.by(1) expect(response).to have_http_status(:created) @@ -137,8 +139,8 @@ RSpec.describe MatchesController, type: :controller do expect(last_match.reload.position).to eq(original_first_position) body = deserialize_response(response) - expect(body.map { |match| match[:id] }).to match_array([first_match.id, last_match.id]) - expect(body.map { |match| match[:position] }).to match_array([original_first_position, original_last_position]) + expect(body.pluck(:id)).to match_array([first_match.id, last_match.id]) + expect(body.pluck(:position)).to match_array([original_first_position, original_last_position]) end it 'rejects matches from different groups' do diff --git a/spec/controllers/stages_controller_spec.rb b/spec/controllers/stages_controller_spec.rb index 9311ea7..ab59bb3 100644 --- a/spec/controllers/stages_controller_spec.rb +++ b/spec/controllers/stages_controller_spec.rb @@ -26,7 +26,7 @@ RSpec.describe StagesController, type: :controller do match.save! end - UpdateGroupsGroupScoresAndSave.call(group: group) + UpdateGroupsGroupScoresAndSave.call(group:) end stage.reload @@ -179,17 +179,20 @@ RSpec.describe StagesController, type: :controller do before do tournament = group_stage.tournament - tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, + intermediate_round_participants_amount: 0) group = create(:group, stage: group_stage, match_count: 0, number: 1) group_stage.groups = [group] - team_a = create(:team, tournament: tournament, name: 'A') - team_b = create(:team, tournament: tournament, name: 'B') - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + team_a = create(:team, tournament:, name: 'A') + team_b = create(:team, tournament:, name: 'B') + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) + score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, + received_points: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, + received_points: 1) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) apply_authentication_headers_for group_stage.owner @@ -215,17 +218,20 @@ RSpec.describe StagesController, type: :controller do before do tournament = group_stage.tournament - tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, + intermediate_round_participants_amount: 0) group = create(:group, stage: group_stage, match_count: 0, number: 1) group_stage.groups = [group] - team_a = create(:team, tournament: tournament, name: 'A') - team_b = create(:team, tournament: tournament, name: 'B') - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + team_a = create(:team, tournament:, name: 'A') + team_b = create(:team, tournament:, name: 'B') + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) + score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, + received_points: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, + received_points: 1) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) apply_authentication_headers_for group_stage.owner diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index 9a9cb9a..4e7e0aa 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -24,7 +24,7 @@ RSpec.describe TournamentsController, type: :controller do get :index tournaments = deserialize_response response public_tournaments = tournaments.select { |t| t[:public] } - expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] }) + expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id)) end end @@ -35,15 +35,15 @@ RSpec.describe TournamentsController, type: :controller do it 'returns all private tournaments' do apply_authentication_headers_for @another_user - get :index, params: params + get(:index, params:) tournaments = deserialize_response response - private_tournaments = Tournament.where(owner: @another_user, public: false).map { |t| t[:id] } - returned_private_tournaments = tournaments.filter { |t| !t[:public] }.map { |t| t[:id] } + private_tournaments = Tournament.where(owner: @another_user, public: false).pluck(:id) + returned_private_tournaments = tournaments.filter { |t| !t[:public] }.pluck(:id) expect(returned_private_tournaments).to match_array(private_tournaments) end it 'returns no private tournaments for unauthenticated users' do - get :index, params: params + get(:index, params:) tournaments = deserialize_response response private_tournaments = tournaments.reject { |t| t[:public] } expect(private_tournaments.size).to eq(0) @@ -51,14 +51,14 @@ RSpec.describe TournamentsController, type: :controller do it 'returns no private tournaments owned by another user' do apply_authentication_headers_for @user - get :index, params: params + get(:index, params:) tournaments = deserialize_response response - expect(tournaments.map { |t| t[:id] }).not_to include(@private_tournament.id) + expect(tournaments.pluck(:id)).not_to include(@private_tournament.id) end it 'returns no public tournaments' do apply_authentication_headers_for @another_user - get :index, params: params + get(:index, params:) tournaments = deserialize_response response expect(tournaments.count { |t| t[:public] }).to eq(0) end @@ -70,15 +70,15 @@ RSpec.describe TournamentsController, type: :controller do end it 'returns all public tournaments' do - get :index, params: params + get(:index, params:) tournaments = deserialize_response response public_tournaments = tournaments.select { |t| t[:public] } - expect(public_tournaments.map { |t| t[:id] }).to match_array(Tournament.where(public: true).map { |t| t[:id] }) + expect(public_tournaments.pluck(:id)).to match_array(Tournament.where(public: true).pluck(:id)) end it 'returns no private tournaments' do apply_authentication_headers_for @another_user - get :index, params: params + get(:index, params:) tournaments = deserialize_response response expect(tournaments.count { |t| !t[:public] }).to eq(0) end @@ -224,7 +224,7 @@ RSpec.describe TournamentsController, type: :controller do describe 'GET #timer' do it 'returns timer state in ISO 8601 format' do timestamp = Time.utc(2026, 4, 13, 18, 0, 0) - @tournament.update!(timestamp: timestamp, timer_mode: 'countdown') + @tournament.update!(timestamp:, timer_mode: 'countdown') get :timer, params: { id: @tournament.to_param } @@ -242,7 +242,7 @@ RSpec.describe TournamentsController, type: :controller do it 'stores timezone-aware timestamp values without shifting them' do timestamp = '2030-04-13T18:00:00+02:00' - patch :set_timer, params: { id: @tournament.to_param, timestamp: timestamp } + patch :set_timer, params: { id: @tournament.to_param, timestamp: } expect(response).to be_successful expect(@tournament.reload.timestamp.iso8601).to eq(Time.iso8601(timestamp).utc.iso8601) @@ -257,7 +257,7 @@ RSpec.describe TournamentsController, type: :controller do expect(response).to be_successful expect(@tournament.reload.timer_mode).to eq('countup') - expect(@tournament.timestamp).to be_between(before_request - 1.second, Time.zone.now + 1.second) + expect(@tournament.timestamp).to be_between(before_request - 1.second, 1.second.from_now) expect(deserialize_response(response)[:timer_mode]).to eq('countup') end diff --git a/spec/e2e/http/api_surface_spec.rb b/spec/e2e/http/api_surface_spec.rb index aebfcaf..2d29e30 100644 --- a/spec/e2e/http/api_surface_spec.rb +++ b/spec/e2e/http/api_surface_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'Backend API surface HTTP E2E' do let(:owner_client) { login_client(email: owner_email, password: owner_password) } let(:other_client) { secondary_client } - let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } + let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) } it 'reports health' do response = anonymous_client.get('/healthz') @@ -34,14 +34,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do email = "http-e2e-#{unique}@example.com" password = 'password123' username = "http-e2e-#{unique}" - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) registration = client.post('/users', body: { - email: email, - password: password, + email:, + password:, password_confirmation: password, - username: username + username: }) - login = TurniereE2E::ApiClient.new(base_url: base_url).login!(email: email, password: password) + login = TurniereE2E::ApiClient.new(base_url:).login!(email:, password:) expect([500, *200..299]).to include(registration[:status]) expect(registration[:json]).not_to eq({}) @@ -59,12 +59,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do invalid_index = anonymous_client.get('/tournaments', params: { type: 'bogus' }) expect(public_index[:status]).to eq(200) - expect(public_index[:json].map { |tournament| tournament[:id] }).to include(public_tournament.fetch(:id)) - expect(public_index[:json].map { |tournament| tournament[:id] }).not_to include(private_tournament.fetch(:id)) + expect(public_index[:json].pluck(:id)).to include(public_tournament.fetch(:id)) + expect(public_index[:json].pluck(:id)).not_to include(private_tournament.fetch(:id)) expect(private_index[:status]).to eq(200) - expect(private_index[:json].map { |tournament| tournament[:id] }).to include(private_tournament.fetch(:id)) - expect(private_index[:json].map { |tournament| tournament[:id] }).not_to include(public_tournament.fetch(:id)) + expect(private_index[:json].pluck(:id)).to include(private_tournament.fetch(:id)) + expect(private_index[:json].pluck(:id)).not_to include(public_tournament.fetch(:id)) expect(simple_show[:status]).to eq(200) expect(simple_show[:json][:id]).to eq(public_tournament.fetch(:id)) @@ -93,10 +93,10 @@ RSpec.describe 'Backend API surface HTTP E2E' do teams: [{ name: 'Solo Team' }] }) - expect(group_stage.fetch(:stages).map { |stage| stage[:level] }).to include(-1) + expect(group_stage.fetch(:stages).pluck(:level)).to include(-1) expect(group_stage.fetch(:teams).count).to eq(16) - expect(playoff_only.fetch(:stages).map { |stage| stage[:level] }.sort).to eq([0, 1]) + expect(playoff_only.fetch(:stages).pluck(:level).sort).to eq([0, 1]) expect(playoff_only.fetch(:teams).count).to eq(4) expect(invalid_teams[:status]).to eq(422) @@ -133,7 +133,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do timestamp_seconds: 120, timestamp: (Time.now.utc + 240).iso8601 }) - timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 'nope' }) + timer_invalid_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", + body: { timestamp_seconds: 'nope' }) timer_past = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timestamp_seconds: 0 }) timer_countup_seconds = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/timer", body: { timer_mode: 'countup', @@ -201,12 +202,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do statistics_before = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/statistics") matches_all = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches") - matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'not_started' }) - matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) + matches_not_started = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", + params: { state: 'not_started' }) + matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches", + params: { state: 'upcoming' }) match_show = anonymous_client.get("/matches/#{match.fetch(:id)}") invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' }) forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) - unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) + unauthenticated_match_update = anonymous_client.patch("/matches/#{match.fetch(:id)}", + body: { state: 'in_progress' }) stage_not_over = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) invalid_stage_update = owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'in_progress' }) forbidden_stage_update = other_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) @@ -225,7 +229,8 @@ RSpec.describe 'Backend API surface HTTP E2E' do score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}") score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 }) forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) - unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) + unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", + body: { points: 3 }) bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) }) restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics") @@ -268,9 +273,9 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(matches_all[:status]).to eq(200) expect(matches_all[:json].count).to eq(tournament_match_count(tournament)) expect(matches_not_started[:status]).to eq(200) - expect(matches_not_started[:json].map { |item| item[:id] }).to include(match.fetch(:id)) + expect(matches_not_started[:json].pluck(:id)).to include(match.fetch(:id)) expect(matches_upcoming[:status]).to eq(200) - expect(matches_upcoming[:json].map { |item| item[:id] }).to include(match.fetch(:id)) + expect(matches_upcoming[:json].pluck(:id)).to include(match.fetch(:id)) expect(match_show[:status]).to eq(200) expect(invalid_match_update[:status]).to eq(422) expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state') @@ -332,35 +337,42 @@ RSpec.describe 'Backend API surface HTTP E2E' do }) swapped_tournament = fetch_tournament(client: owner_client, tournament_id: tournament.fetch(:id)) swapped_group = swapped_tournament.fetch(:stages) - .find { |stage| stage.fetch(:level) == -1 } - .fetch(:groups) - .find { |item| item.fetch(:id) == group.fetch(:id) } + .find { |stage| stage.fetch(:level) == -1 } + .fetch(:groups) + .find { |item| item.fetch(:id) == group.fetch(:id) } swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) } - upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) + upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", + params: { state: 'upcoming' }) finish_group_match_with_winner!(match_id: fourth_match.fetch(:id)) - upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) + upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", + params: { state: 'upcoming' }) expect(upcoming_before[:status]).to eq(200) - expect(upcoming_match_for_group(upcoming_before, group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id)) + expect(upcoming_match_for_group(upcoming_before, + group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id)) expect(swap_response[:status]).to eq(200) - expect(swap_response[:json].map { |match| match[:id] }).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)]) + expect(swap_response[:json].pluck(:id)).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)]) expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id)) expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position)) - expect(swapped_matches.find { |match| match.fetch(:id) == first_match.fetch(:id) }.fetch(:position)).to eq(fourth_match.fetch(:position)) + expect(swapped_matches.find do |match| + match.fetch(:id) == first_match.fetch(:id) + end.fetch(:position)).to eq(fourth_match.fetch(:position)) expect(upcoming_after_swap[:status]).to eq(200) - expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id)) + expect(upcoming_match_for_group(upcoming_after_swap, + group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id)) expect(upcoming_after_finish[:status]).to eq(200) - expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id)) + expect(upcoming_match_for_group(upcoming_after_finish, + group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id)) end def login_client(email:, password:) - client = TurniereE2E::ApiClient.new(base_url: base_url) - response = client.login!(email: email, password: password) + client = TurniereE2E::ApiClient.new(base_url:) + response = client.login!(email:, password:) expect(response[:status]).to eq(200) expect(client.authenticated?).to eq(true) @@ -368,15 +380,15 @@ RSpec.describe 'Backend API surface HTTP E2E' do end def secondary_client - alt_email = ENV['TURNIERE_E2E_ALT_EMAIL'] - alt_password = ENV['TURNIERE_E2E_ALT_PASSWORD'] + alt_email = ENV.fetch('TURNIERE_E2E_ALT_EMAIL', nil) + alt_password = ENV.fetch('TURNIERE_E2E_ALT_PASSWORD', nil) if alt_email && alt_password - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) response = client.login!(email: alt_email, password: alt_password) return client if response[:status] == 200 && client.authenticated? end - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) unique = unique_suffix response = client.post('/users', body: { email: "http-e2e-secondary-#{unique}@example.com", @@ -394,11 +406,11 @@ RSpec.describe 'Backend API surface HTTP E2E' do payload = { name: "#{name_prefix} #{unique_suffix}", description: 'HTTP API surface E2E group-stage tournament', - public: public, + public:, group_stage: true, playoff_teams_amount: 8, teams: 4.times.flat_map do |group_index| - 4.times.map do |seed_index| + Array.new(4) do |seed_index| { name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -409,22 +421,22 @@ RSpec.describe 'Backend API surface HTTP E2E' do response = client.post('/tournaments', body: payload) expect(response[:status]).to eq(201) - fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) + fetch_tournament(client:, tournament_id: response.dig(:json, :id)) end def create_playoff_tournament(client:, public:, name_prefix:) payload = { name: "#{name_prefix} #{unique_suffix}", description: 'HTTP API surface E2E playoff tournament', - public: public, - teams: 4.times.map do |index| + public:, + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end } response = client.post('/tournaments', body: payload) expect(response[:status]).to eq(201) - fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) + fetch_tournament(client:, tournament_id: response.dig(:json, :id)) end def fetch_tournament(client:, tournament_id:) diff --git a/spec/e2e/http/tournament_beamers_websocket_spec.rb b/spec/e2e/http/tournament_beamers_websocket_spec.rb index 4588172..b976834 100644 --- a/spec/e2e/http/tournament_beamers_websocket_spec.rb +++ b/spec/e2e/http/tournament_beamers_websocket_spec.rb @@ -80,7 +80,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do name: "#{name_prefix} #{SecureRandom.hex(3)}", description: 'Websocket beamer tournament', public: true, - teams: 4.times.map do |index| + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end }) @@ -89,13 +89,13 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do end def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max) + payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max) return payload if payload.fetch('type') == event_type && payload.dig('beamer', 'display_state') == display_state - raise "beamer websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end end diff --git a/spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb b/spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb index 19eedf8..1e17b21 100644 --- a/spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb @@ -40,10 +40,12 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id)) cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url) - cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id), beamer_name: follower_default_beamer.fetch(:name)) + cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id), + beamer_name: follower_default_beamer.fetch(:name)) cable_client.wait_for_beamer_payload! - source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id), beamer_name: DEFAULT_BEAMER_NAME) + source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id), + beamer_name: DEFAULT_BEAMER_NAME) update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: { display_state: 'groups' }) @@ -71,7 +73,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do name: "#{name_prefix} #{SecureRandom.hex(3)}", description: 'Follower beamer websocket source tournament', public: true, - teams: 4.times.map do |index| + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end }) @@ -107,25 +109,25 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do end def wait_for_default_beamer!(tournament_id:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do beamer = fetch_beamer(base_url: follower_base_url, tournament_id:, beamer_name: DEFAULT_BEAMER_NAME) return beamer if beamer.fetch(:name) == DEFAULT_BEAMER_NAME - raise "follower default beamer not present within #{timeout}s" if Time.now >= deadline + raise "follower default beamer not present within #{timeout}s" if Time.zone.now >= deadline sleep 0.5 end end def wait_for_beamer_payload!(cable_client, event_type:, display_state:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.now, 0.1].max) + payload = cable_client.wait_for_beamer_payload!(timeout: [deadline - Time.zone.now, 0.1].max) return payload if payload.fetch('type') == event_type && payload.dig('beamer', 'display_state') == display_state - raise "follower beamer websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "follower beamer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end end diff --git a/spec/e2e/http/tournament_follow_sync_spec.rb b/spec/e2e/http/tournament_follow_sync_spec.rb index 1800ad6..c069cf5 100644 --- a/spec/e2e/http/tournament_follow_sync_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_spec.rb @@ -23,7 +23,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:sync_token) { "sync-#{SecureRandom.hex(8)}" } let(:source_owner_client) { login_client(base_url: source_base_url, email: owner_email, password: owner_password) } - let(:follower_owner_client) { login_client(base_url: follower_base_url, email: owner_email, password: owner_password) } + let(:follower_owner_client) do + login_client(base_url: follower_base_url, email: owner_email, password: owner_password) + end let(:source_anonymous_client) { TurniereE2E::ApiClient.new(base_url: source_base_url) } let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) } @@ -39,27 +41,28 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do log_phase!('configure sync and verify initial replication') configure_sync = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: { - sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state", - sync_auth_token: sync_token - }) + sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state", + sync_auth_token: sync_token + }) expect(configure_sync[:status]).to eq(200) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + exercise_unrelated_requests!(source_noise:, follower_noise:) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(tournament_signature(follower)).to eq(tournament_signature(source)) - expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty + expect(synced_team_id_pairs(source:, follower:)).not_to be_empty log_phase!('swap matches on leader and verify follower behavior') source_group_stage = source.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } - first_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first + first_source_group = source_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) } second_source_group = source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }[1] first_group_matches = first_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) } first_position_match = first_group_matches.fetch(0) fourth_position_match = first_group_matches.fetch(3) - wrong_group_match = second_source_group.fetch(:matches).sort_by { |match| match.fetch(:position) }.first + wrong_group_match = second_source_group.fetch(:matches).min_by { |match| match.fetch(:position) } follower_group_stage = follower.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } - first_follower_group = follower_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }.first + first_follower_group = follower_group_stage.fetch(:groups).min_by { |group| group.fetch(:number) } follower_first_position_match = first_follower_group.fetch(:matches).find do |match| match_team_names(match) == match_team_names(first_position_match) end @@ -72,21 +75,24 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do }) expect(leader_swap[:status]).to eq(200) expect(leader_swap[:json].map { |match| match.fetch(:position) }).to match_array([ - first_position_match.fetch(:position), - fourth_position_match.fetch(:position) - ]) + first_position_match.fetch(:position), + fourth_position_match.fetch(:position) + ]) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(group_match_positions(source, group_number: first_source_group.fetch(:number))).to eq( group_match_positions(follower, group_number: first_follower_group.fetch(:number)) ) - expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq( - fourth_position_match.fetch(:position) - ) - expect(group_match_positions(source, group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq( - first_position_match.fetch(:position) - ) + expect(group_match_positions(source, + group_number: first_source_group.fetch(:number)).fetch(match_team_names(first_position_match))).to eq( + fourth_position_match.fetch(:position) + ) + expect(group_match_positions(source, + group_number: first_source_group.fetch(:number)).fetch(match_team_names(fourth_position_match))).to eq( + first_position_match.fetch(:position) + ) follower_before_invalid_swap = tournament_signature(follower) source_before_invalid_swap = tournament_signature(source) @@ -97,7 +103,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do expect(wrong_group_swap[:status]).to eq(422) expect(wrong_group_swap.dig(:json, :error)).to eq('Matches must be different group matches from the same group') source_after_invalid_swap = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id)) + follower_after_invalid_swap = fetch_tournament(client: follower_anonymous_client, + tournament_id: follower.fetch(:id)) expect(tournament_signature(source_after_invalid_swap)).to eq(source_before_invalid_swap) expect(tournament_signature(follower_after_invalid_swap)).to eq(follower_before_invalid_swap) @@ -111,15 +118,15 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do log_phase!('tighten playoff cutoff and rename teams') update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: { - playoff_teams_amount: 4, - instant_finalists_amount: 4, - intermediate_round_participants_amount: 0 - }) + playoff_teams_amount: 4, + instant_finalists_amount: 4, + intermediate_round_participants_amount: 0 + }) expect(update_cutoff[:status]).to eq(200) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + exercise_unrelated_requests!(source_noise:, follower_noise:) source_group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - .fetch(:stages).find { |stage| stage.fetch(:level) == -1 } + .fetch(:stages).find { |stage| stage.fetch(:level) == -1 } rename_source_tournament_teams!( source_tournament_id: source.fetch(:id), team_names: [ @@ -127,20 +134,25 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do group_team_map(source_group_stage.fetch(:groups).last).fetch(2).fetch(:name) ] ) - source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise', range: 2..3)) - follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', range: 3..4)) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Midway Noise', + range: 2..3)) + follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Midway Noise', + range: 3..4)) + exercise_unrelated_requests!(source_noise:, follower_noise:) log_phase!('play group stage with deciders') - play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by { |group| group.fetch(:number) }) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + play_group_with_decider_lifecycle!(source_id: source.fetch(:id), groups: source_group_stage.fetch(:groups).sort_by do |group| + group.fetch(:number) + end) + exercise_unrelated_requests!(source_noise:, follower_noise:) log_phase!('verify playoff generation replicated') source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(tournament_signature(follower)).to eq(tournament_signature(source)) expect(follower.fetch(:stages).map { |stage| stage.fetch(:level) }).to include(-1, 0, 1) - expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty + expect(synced_team_id_pairs(source:, follower:)).not_to be_empty expect(source.fetch(:team_action_lists)).to eq([]) expect(follower.fetch(:team_action_lists)).to eq([]) @@ -149,7 +161,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do expect(created_list.fetch(:key)).to eq(playoff_token_list_key) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(playoff_token_list(source)).not_to be_nil expect(playoff_token_list(follower)).not_to be_nil expect(playoff_token_list(follower).fetch(:team_action_items).map { |item| item.dig(:team, :name) }) @@ -173,44 +186,54 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do log_phase!('mark playoff token collected on source through business key route and verify follower mirrors it') source_token_item = playoff_token_list(source).fetch(:team_action_items).first source_token_update = source_owner_client.patch( - "/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig(:team, :id)}", + "/tournaments/#{source.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{source_token_item.dig( + :team, :id + )}", body: { completed: true } ) expect(source_token_update[:status]).to eq(200) source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(playoff_token_item_completed?(source, team_name: source_token_item.dig(:team, :name))).to eq(true) expect(playoff_token_item_completed?(follower, team_name: source_token_item.dig(:team, :name))).to eq(true) follower_token_item = find_team_action_item(follower, team_name: source_token_item.dig(:team, :name)) locked_token_update = follower_owner_client.patch( - "/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig(:team, :id)}", + "/tournaments/#{follower.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{follower_token_item.dig( + :team, :id + )}", body: { completed: false } ) expect(locked_token_update[:status]).to eq(423) expect(locked_token_update.dig(:json, :error)).to eq('Tournament is in read only follower mode') - source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise', range: 2..4)) - follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', range: 2..5)) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + source_noise.concat(seed_unrelated_tournaments!(client: source_owner_client, prefix: 'Source Playoff Noise', + range: 2..4)) + follower_noise.concat(seed_unrelated_tournaments!(client: follower_owner_client, prefix: 'Follower Playoff Noise', + range: 2..5)) + exercise_unrelated_requests!(source_noise:, follower_noise:) log_phase!('finish playoff bracket') finish_playoff_bracket!(source_id: source.fetch(:id)) - exercise_unrelated_requests!(source_noise: source_noise, follower_noise: follower_noise) + exercise_unrelated_requests!(source_noise:, follower_noise:) log_phase!('verify final follower state and takeover') source = fetch_tournament(client: source_anonymous_client, tournament_id: source.fetch(:id)) - follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), follower_tournament_id: follower.fetch(:id)) + follower = wait_for_tournament_sync!(source_tournament_id: source.fetch(:id), + follower_tournament_id: follower.fetch(:id)) expect(tournament_signature(follower)).to eq(tournament_signature(source)) - expect(synced_team_id_pairs(source: source, follower: follower)).not_to be_empty + expect(synced_team_id_pairs(source:, follower:)).not_to be_empty - disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}", body: { read_only_mode: false }) + disable_follower = follower_owner_client.patch("/tournaments/#{follower.fetch(:id)}", + body: { read_only_mode: false }) expect(disable_follower[:status]).to eq(200) follower_after_disable = fetch_tournament(client: follower_anonymous_client, tournament_id: follower.fetch(:id)) writable_team = follower_after_disable.fetch(:teams).first - takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}", body: { name: "#{writable_team.fetch(:name)} takeover" }) + takeover_update = follower_owner_client.patch("/teams/#{writable_team.fetch(:id)}", + body: { name: "#{writable_team.fetch(:name)} takeover" }) expect(takeover_update[:status]).to eq(200) end @@ -220,8 +243,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do end def login_client(base_url:, email:, password:) - client = TurniereE2E::ApiClient.new(base_url: base_url) - response = client.login!(email: email, password: password) + client = TurniereE2E::ApiClient.new(base_url:) + response = client.login!(email:, password:) expect(response[:status]).to eq(200) expect(client.authenticated?).to eq(true) @@ -232,11 +255,11 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do payload = { name: "#{name_prefix} #{unique_suffix}", description: 'Follower sync source tournament', - public: public, + public:, group_stage: true, playoff_teams_amount: 8, teams: 4.times.flat_map do |group_index| - 4.times.map do |seed_index| + Array.new(4) do |seed_index| { name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -247,34 +270,34 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do response = client.post('/tournaments', body: payload) expect(response[:status]).to eq(201) - fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) + fetch_tournament(client:, tournament_id: response.dig(:json, :id)) end def create_playoff_tournament(client:, public:, name_prefix:) payload = { name: "#{name_prefix} #{unique_suffix}", description: 'Unrelated playoff tournament', - public: public, - teams: 4.times.map do |index| + public:, + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" } end } response = client.post('/tournaments', body: payload) expect(response[:status]).to eq(201) - fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) + fetch_tournament(client:, tournament_id: response.dig(:json, :id)) end def create_follower_tournament(client:, public:, name_prefix:) response = client.post('/tournaments', body: { name: "#{name_prefix} #{unique_suffix}", description: 'Follower tournament', - public: public, + public:, read_only_mode: true, sync_auth_token: sync_token }) expect(response[:status]).to eq(201) - fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) + fetch_tournament(client:, tournament_id: response.dig(:json, :id)) end def fetch_tournament(client:, tournament_id:) @@ -298,9 +321,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do amount = rand(range) Array.new(amount) do |index| if index.even? - create_playoff_tournament(client: client, public: false, name_prefix: "#{prefix} Playoff #{index}") + create_playoff_tournament(client:, public: false, name_prefix: "#{prefix} Playoff #{index}") else - create_group_stage_tournament(client: client, public: true, name_prefix: "#{prefix} Groups #{index}") + create_group_stage_tournament(client:, public: true, name_prefix: "#{prefix} Groups #{index}") end end end @@ -316,7 +339,7 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do update = client.patch("/tournaments/#{tournament.fetch(:id)}", body: { name: updated_name }) expect(update[:status]).to eq(200) - refreshed = fetch_tournament(client: client, tournament_id: tournament.fetch(:id)) + refreshed = fetch_tournament(client:, tournament_id: tournament.fetch(:id)) team = refreshed.fetch(:teams).first return if team.nil? @@ -340,21 +363,21 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do end def wait_for_tournament_sync!(source_tournament_id:, follower_tournament_id:, timeout: 20) - deadline = Time.now + timeout - last_progress_at = Time.now + deadline = Time.zone.now + timeout + last_progress_at = Time.zone.now loop do source = fetch_tournament(client: source_anonymous_client, tournament_id: source_tournament_id) follower = fetch_tournament(client: follower_anonymous_client, tournament_id: follower_tournament_id) return follower if tournament_signature(source) == tournament_signature(follower) - if Time.now - last_progress_at >= 2 + if Time.zone.now - last_progress_at >= 2 puts "sync wait source=#{sync_progress_signature(source).inspect} follower=#{sync_progress_signature(follower).inspect}" $stdout.flush - last_progress_at = Time.now + last_progress_at = Time.zone.now end - if Time.now >= deadline + if Time.zone.now >= deadline raise <<~ERROR Follower did not catch up within #{timeout}s source_signature=#{tournament_signature(source).inspect} @@ -374,8 +397,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do end def synced_team_id_pairs(source:, follower:) - source_by_name = source.fetch(:teams).each_with_object({}) do |team, teams_by_name| - teams_by_name[team.fetch(:name)] = team + source_by_name = source.fetch(:teams).index_by do |team| + team.fetch(:name) end follower.fetch(:teams).filter_map do |team| source_team = source_by_name[team.fetch(:name)] @@ -396,17 +419,18 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do end group_stage = fetch_tournament(client: source_anonymous_client, tournament_id: source_id) - .fetch(:stages).find { |stage| stage.fetch(:level) == -1 } + .fetch(:stages).find { |stage| stage.fetch(:level) == -1 } stage_finish = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) expect(stage_finish[:status]).to eq(422) - expect(stage_finish.dig(:json, :error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place') + expect(stage_finish.dig(:json, + :error)).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place') expect(stage_finish.dig(:json, :blocking_ties).size).to be >= 2 stage_finish.fetch(:json).fetch(:blocking_ties).each_with_index do |blocking_tie, index| decider_create = source_owner_client.post("/groups/#{blocking_tie.fetch(:group_id)}/matches", body: { - team_ids: blocking_tie.fetch(:team_ids) - }) + team_ids: blocking_tie.fetch(:team_ids) + }) expect(decider_create[:status]).to eq(201) decider_match = decider_create.fetch(:json) @@ -422,7 +446,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do expect(finish_decider[:status]).to eq(200) end - finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", body: { state: 'finished' }) + finish_stage_after_decider = source_owner_client.patch("/stages/#{group_stage.fetch(:id)}", + body: { state: 'finished' }) expect(finish_stage_after_decider[:status]).to eq(200) end @@ -464,10 +489,13 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do loop do tournament = fetch_tournament(client: source_anonymous_client, tournament_id: source_id) playoff_matches = tournament.fetch(:stages) - .select { |stage| stage.fetch(:level) >= 0 } - .flat_map { |stage| stage.fetch(:matches) } - .select { |match| %w[not_started in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2 } - .sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] } + .select { |stage| stage.fetch(:level) >= 0 } + .flat_map { |stage| stage.fetch(:matches) } + .select do |match| + %w[not_started + in_progress].include?(match.fetch(:state)) && match.fetch(:match_scores).size == 2 + end + .sort_by { |match| [match.dig(:stage, :id) || 0, match.fetch(:position)] } break if playoff_matches.empty? playoff_matches.each_with_index do |match, index| @@ -488,7 +516,8 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do ordered_scores = match.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } [first_score, second_score].each_with_index do |points, index| - response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) + response = source_owner_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", + body: { points: }) expect(response[:status]).to eq(200) end @@ -504,7 +533,9 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do instant_finalists_amount: tournament.fetch(:instant_finalists_amount), intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount), timestamp: tournament[:timestamp], - teams: tournament.fetch(:teams).map { |team| team.slice(:name, :advancing_from_group_stage) }.sort_by { |team| team.fetch(:name) }, + teams: tournament.fetch(:teams).map do |team| + team.slice(:name, :advancing_from_group_stage) + end.sort_by { |team| team.fetch(:name) }, team_action_lists: tournament.fetch(:team_action_lists, []).sort_by { |list| list.fetch(:key) }.map do |list| { key: list.fetch(:key), @@ -596,6 +627,6 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do end def playoff_token_item_completed?(tournament, team_name:) - find_team_action_item(tournament, team_name: team_name).fetch(:completed) + find_team_action_item(tournament, team_name:).fetch(:completed) end end diff --git a/spec/e2e/http/tournament_follow_sync_websocket_spec.rb b/spec/e2e/http/tournament_follow_sync_websocket_spec.rb index f748a93..339b50e 100644 --- a/spec/e2e/http/tournament_follow_sync_websocket_spec.rb +++ b/spec/e2e/http/tournament_follow_sync_websocket_spec.rb @@ -27,7 +27,8 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do let(:follower_anonymous_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) } it 'broadcasts follower websocket updates after source sync import' do - source = create_group_stage_tournament(client: source_client, base_url: source_base_url, name_prefix: 'Ws Sync Source') + source = create_group_stage_tournament(client: source_client, base_url: source_base_url, + name_prefix: 'Ws Sync Source') follower = create_follower_tournament(name_prefix: 'Ws Sync Follower') configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: { @@ -61,7 +62,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do end def login_client(base_url:) - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) response = client.login!(email: owner_email, password: owner_password) expect(response[:status]).to eq(200) client @@ -75,7 +76,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do group_stage: true, playoff_teams_amount: 4, teams: 2.times.flat_map do |group_index| - 4.times.map do |seed_index| + Array.new(4) do |seed_index| { name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -84,7 +85,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do end }) expect(response[:status]).to eq(201) - fetch_tournament(base_url: base_url, tournament_id: response.dig(:json, :id)) + fetch_tournament(base_url:, tournament_id: response.dig(:json, :id)) end def create_follower_tournament(name_prefix:) @@ -101,7 +102,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do end def fetch_tournament(base_url:, tournament_id:) - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) response = client.get("/tournaments/#{tournament_id}") expect(response[:status]).to eq(200) response.fetch(:json) @@ -141,7 +142,7 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } [first_score, second_score].each_with_index do |points, index| - score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) + score_update = source_client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: }) expect(score_update[:status]).to eq(200) end @@ -150,24 +151,24 @@ RSpec.describe 'Tournament follower sync websocket HTTP E2E' do end def wait_for_list_presence!(tournament_id, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - follower = fetch_tournament(base_url: follower_base_url, tournament_id: tournament_id) + follower = fetch_tournament(base_url: follower_base_url, tournament_id:) return follower if follower.fetch(:team_action_lists, []).any? - raise "follower list not present within #{timeout}s" if Time.now >= deadline + raise "follower list not present within #{timeout}s" if Time.zone.now >= deadline sleep 0.5 end end def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max) - return payload if team_action_item_completed?(payload, team_name: team_name) == completed + payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max) + return payload if team_action_item_completed?(payload, team_name:) == completed - raise "follower websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "follower websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end diff --git a/spec/e2e/http/tournament_lifecycle_spec.rb b/spec/e2e/http/tournament_lifecycle_spec.rb index 36f8ee2..e68079e 100644 --- a/spec/e2e/http/tournament_lifecycle_spec.rb +++ b/spec/e2e/http/tournament_lifecycle_spec.rb @@ -14,10 +14,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') } let(:runner) do TurniereE2E::ScenarioRunner.new( - base_url: base_url, + base_url:, email: ENV.fetch('TURNIERE_E2E_EMAIL'), password: ENV.fetch('TURNIERE_E2E_PASSWORD'), - username: ENV['TURNIERE_E2E_USERNAME'] + username: ENV.fetch('TURNIERE_E2E_USERNAME', nil) ) end @@ -31,7 +31,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do expect(result.dig(:checkpoints, :statistics, :group_scores_count)).to eq(16) playoffs_created = result.dig(:checkpoints, :playoffs_created, :stage_summaries) - playoff_entry_stage = playoffs_created.select { |stage| stage[:level] != -1 }.max_by { |stage| stage[:level] } + playoff_entry_stage = playoffs_created.reject { |stage| stage[:level] == -1 }.max_by { |stage| stage[:level] } expect(playoff_entry_stage[:match_states].count).to eq(4) final_attempt = result.dig(:checkpoints, :final_finish_attempted, :finish_attempt) @@ -49,8 +49,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do expect(created[:intermediate_round_participants_amount]).to eq(2) playoff_entry_stage = result.dig(:checkpoints, :playoffs_created, :stage_summaries) - .select { |stage| stage[:level] != -1 } - .max_by { |stage| stage[:level] } + .reject { |stage| stage[:level] == -1 } + .max_by { |stage| stage[:level] } expect(playoff_entry_stage[:state]).to eq('intermediate_stage') expect(playoff_entry_stage[:match_states].count('single_team')).to eq(3) expect(playoff_entry_stage[:match_states].count('not_started')).to eq(1) @@ -76,7 +76,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do expect(positions.fetch('G1-S2')).to eq(expected_positions[1]) if direct_result == :drawn - expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) + expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, + :received_points)) .to eq( team_name: 'G1-S1', position: 1, @@ -85,7 +86,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do scored_points: 4, received_points: 1 ) - expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) + expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, + :received_points)) .to eq( team_name: 'G1-S2', position: 1, @@ -96,9 +98,10 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do ) else expected_first_group_points = direct_result == :lost ? 9 : 6 - expected_second_group_points = direct_result == :lost ? 6 : 6 + expected_second_group_points = 6 - expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) + expect(ordered_scores[0].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, + :received_points)) .to eq( team_name: expected_positions[0] == 1 ? 'G1-S1' : 'G1-S2', position: 1, @@ -107,7 +110,8 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do scored_points: 5, received_points: 2 ) - expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, :received_points)) + expect(ordered_scores[1].slice(:team_name, :position, :group_points, :difference_in_points, :scored_points, + :received_points)) .to eq( team_name: expected_positions[0] == 1 ? 'G1-S2' : 'G1-S1', position: 2, @@ -122,7 +126,7 @@ RSpec.describe 'Tournament lifecycle HTTP E2E' do [2, 3, 5, 8].each do |team_count| it "drives a #{team_count}-team playoff-only tournament over HTTP to completion" do - result = runner.run_playoff_only(team_count: team_count) + result = runner.run_playoff_only(team_count:) completed_stages = result.dig(:checkpoints, :completed, :stage_summaries) final_stage = completed_stages.find { |stage| stage[:level] == 0 } diff --git a/spec/e2e/http/tournament_rendering_spec.rb b/spec/e2e/http/tournament_rendering_spec.rb index cf89137..074bb53 100644 --- a/spec/e2e/http/tournament_rendering_spec.rb +++ b/spec/e2e/http/tournament_rendering_spec.rb @@ -15,17 +15,20 @@ RSpec.describe 'Tournament rendering HTTP E2E' do let(:expect_profiling) { ENV.fetch('TURNIERE_E2E_EXPECT_PROFILING', 'true') == 'true' } let(:runner) do TurniereE2E::ScenarioRunner.new( - base_url: base_url, + base_url:, email: ENV.fetch('TURNIERE_E2E_EMAIL'), password: ENV.fetch('TURNIERE_E2E_PASSWORD'), - username: ENV['TURNIERE_E2E_USERNAME'] + username: ENV.fetch('TURNIERE_E2E_USERNAME', nil) ) end [ - { group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, max_server_duration_ms: 1000 }, - { group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000, max_server_duration_ms: 5000 }, - { group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000, max_server_duration_ms: 5000 } + { group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, + max_server_duration_ms: 1000 }, + { group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000, + max_server_duration_ms: 5000 }, + { group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000, + max_server_duration_ms: 5000 } ].each do |scenario| it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do result = runner.run_group_stage_render_profile( diff --git a/spec/e2e/http/tournament_sync_test_spec.rb b/spec/e2e/http/tournament_sync_test_spec.rb index 1d82f59..f63bfc2 100644 --- a/spec/e2e/http/tournament_sync_test_spec.rb +++ b/spec/e2e/http/tournament_sync_test_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:sync_token) { "sync-test-#{SecureRandom.hex(6)}" } let(:client) { login_client } - let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } + let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) } it 'roundtrips current snapshot into follower tournament through test_sync endpoint' do follower = create_follower_tournament(name_prefix: 'Sync Test Follower') @@ -74,7 +74,8 @@ RSpec.describe 'Tournament sync test HTTP E2E' do expect(update_by_item_id[:status]).to eq(200) update_by_business_key = client.patch( - "/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}", + "/tournaments/#{leader.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, + :id)}", body: { completed: true } ) expect(update_by_business_key[:status]).to eq(200) @@ -118,7 +119,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do end def login_client - api_client = TurniereE2E::ApiClient.new(base_url: base_url) + api_client = TurniereE2E::ApiClient.new(base_url:) response = api_client.login!(email: owner_email, password: owner_password) expect(response[:status]).to eq(200) @@ -133,7 +134,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do group_stage: true, playoff_teams_amount: 4, teams: 2.times.flat_map do |group_index| - 4.times.map do |seed_index| + Array.new(4) do |seed_index| { name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -141,10 +142,10 @@ RSpec.describe 'Tournament sync test HTTP E2E' do end end } - body[:sync_target_url] = sync_target_url unless sync_target_url.nil? || sync_target_url.empty? - body[:sync_auth_token] = sync_auth_token unless sync_auth_token.nil? || sync_auth_token.empty? + body[:sync_target_url] = sync_target_url if sync_target_url.present? + body[:sync_auth_token] = sync_auth_token if sync_auth_token.present? - response = client.post('/tournaments', body: body) + response = client.post('/tournaments', body:) expect(response[:status]).to eq(201) fetch_tournament(response.dig(:json, :id)) end @@ -201,7 +202,7 @@ RSpec.describe 'Tournament sync test HTTP E2E' do ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } [first_score, second_score].each_with_index do |points, index| - score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) + score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: }) expect(score_update[:status]).to eq(200) end diff --git a/spec/e2e/http/tournament_team_action_lists_websocket_spec.rb b/spec/e2e/http/tournament_team_action_lists_websocket_spec.rb index d61149e..4f3506c 100644 --- a/spec/e2e/http/tournament_team_action_lists_websocket_spec.rb +++ b/spec/e2e/http/tournament_team_action_lists_websocket_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') } let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') } let(:client) { login_client } - let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url: base_url) } + let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) } it 'broadcasts initial and updated team action list state over websocket' do tournament = create_group_stage_tournament(name_prefix: 'Ws Leader') @@ -30,7 +30,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do tournament = fetch_tournament(tournament.fetch(:id)) token_list = find_playoff_token_list!(tournament) - cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url) + cable_client = TurniereE2E::ActionCableClient.new(base_url:) cable_client.subscribe_team_action_lists!(tournament_id: tournament.fetch(:id)) initial_payload = cable_client.wait_for_payload! @@ -50,7 +50,8 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do expect(team_action_item_completed?(first_update, team_name: first_item.dig(:team, :name))).to eq(true) update_by_business_key = client.patch( - "/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, :id)}", + "/tournaments/#{tournament.fetch(:id)}/team_action_lists/#{playoff_token_list_key}/teams/#{second_item.dig(:team, + :id)}", body: { completed: true } ) expect(update_by_business_key[:status]).to eq(200) @@ -67,7 +68,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do end def login_client - api_client = TurniereE2E::ApiClient.new(base_url: base_url) + api_client = TurniereE2E::ApiClient.new(base_url:) response = api_client.login!(email: owner_email, password: owner_password) expect(response[:status]).to eq(200) @@ -82,7 +83,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do group_stage: true, playoff_teams_amount: 4, teams: 2.times.flat_map do |group_index| - 4.times.map do |seed_index| + Array.new(4) do |seed_index| { name: "#{name_prefix.tr(' ', '')}-G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -134,7 +135,7 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do ordered_scores = match_response.fetch(:json).fetch(:match_scores).sort_by { |score| score.fetch(:id) } [first_score, second_score].each_with_index do |points, index| - score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: points }) + score_update = client.patch("/match_scores/#{ordered_scores[index].fetch(:id)}", body: { points: }) expect(score_update[:status]).to eq(200) end @@ -143,12 +144,12 @@ RSpec.describe 'Tournament team action lists websocket HTTP E2E' do end def wait_for_team_action_list_state!(cable_client, team_name:, completed:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_payload!(timeout: [deadline - Time.now, 0.1].max) - return payload if team_action_item_completed?(payload, team_name: team_name) == completed + payload = cable_client.wait_for_payload!(timeout: [deadline - Time.zone.now, 0.1].max) + return payload if team_action_item_completed?(payload, team_name:) == completed - raise "team action websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "team action websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end diff --git a/spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb b/spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb index 3b8c364..e933315 100644 --- a/spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb +++ b/spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do end def login_client(base_url:) - client = TurniereE2E::ApiClient.new(base_url: base_url) + client = TurniereE2E::ApiClient.new(base_url:) response = client.login!(email: owner_email, password: owner_password) expect(response[:status]).to eq(200) client @@ -74,7 +74,7 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do name: "#{name_prefix} #{SecureRandom.hex(3)}", description: 'Follower websocket timer source tournament', public: false, - teams: 4.times.map do |index| + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-#{index + 1}" } end }) @@ -96,12 +96,12 @@ RSpec.describe 'Tournament timer follower sync websocket HTTP E2E' do end def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max) + payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max) return payload if payload.fetch('timer_mode') == timer_mode - raise "follower timer websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "follower timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end end diff --git a/spec/e2e/http/tournament_timer_websocket_spec.rb b/spec/e2e/http/tournament_timer_websocket_spec.rb index 1431476..435dad3 100644 --- a/spec/e2e/http/tournament_timer_websocket_spec.rb +++ b/spec/e2e/http/tournament_timer_websocket_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do it 'broadcasts timer state changes over websocket' do tournament = create_playoff_tournament(name_prefix: 'Ws Timer') - cable_client = TurniereE2E::ActionCableClient.new(base_url: base_url) + cable_client = TurniereE2E::ActionCableClient.new(base_url:) cable_client.subscribe_timer!(tournament_id: tournament.fetch(:id)) initial_payload = cable_client.wait_for_timer_payload! @@ -54,7 +54,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do end def login_client - api_client = TurniereE2E::ApiClient.new(base_url: base_url) + api_client = TurniereE2E::ApiClient.new(base_url:) response = api_client.login!(email: owner_email, password: owner_password) expect(response[:status]).to eq(200) @@ -66,7 +66,7 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do name: "#{name_prefix} #{SecureRandom.hex(3)}", description: 'Websocket timer tournament', public: false, - teams: 4.times.map do |index| + teams: Array.new(4) do |index| { name: "#{name_prefix.tr(' ', '')}-#{index + 1}" } end }) @@ -75,12 +75,12 @@ RSpec.describe 'Tournament timer websocket HTTP E2E' do end def wait_for_timer_state!(cable_client, timer_mode:, timeout: 20) - deadline = Time.now + timeout + deadline = Time.zone.now + timeout loop do - payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.now, 0.1].max) + payload = cable_client.wait_for_timer_payload!(timeout: [deadline - Time.zone.now, 0.1].max) return payload if payload.fetch('timer_mode') == timer_mode - raise "timer websocket payload not received within #{timeout}s" if Time.now >= deadline + raise "timer websocket payload not received within #{timeout}s" if Time.zone.now >= deadline end end end diff --git a/spec/factories/beamers.rb b/spec/factories/beamers.rb index 0d6f3a0..3b5d51f 100644 --- a/spec/factories/beamers.rb +++ b/spec/factories/beamers.rb @@ -10,7 +10,7 @@ FactoryBot.define do name { Beamer::DEFAULT_NAME } initialize_with do - tournament.beamers.find_or_initialize_by(name: name) + tournament.beamers.find_or_initialize_by(name:) end end end diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index ba375c5..55cc259 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -11,9 +11,9 @@ FactoryBot.define do stage after(:create) do |group, evaluator| - create_list(evaluator.match_factory, evaluator.match_count, group: group) + create_list(evaluator.match_factory, evaluator.match_count, group:) group.group_scores = group.teams.map do |team| - create(:group_score, team: team, group: group) + create(:group_score, team:, group:) end end end diff --git a/spec/factories/tournaments.rb b/spec/factories/tournaments.rb index 4e2ab21..acf464f 100644 --- a/spec/factories/tournaments.rb +++ b/spec/factories/tournaments.rb @@ -13,17 +13,14 @@ FactoryBot.define do intermediate_round_participants_amount { 0 } end after(:create) do |tournament, evaluator| - if evaluator.teams.present? - tournament.teams = evaluator.teams - else - tournament.teams = create_list(:team, evaluator.teams_count, tournament: tournament) - end + tournament.teams = (evaluator.teams.presence || create_list(:team, evaluator.teams_count, tournament:)) tournament.playoff_teams_amount = evaluator.playoff_teams_amount tournament.instant_finalists_amount = evaluator.instant_finalists_amount tournament.intermediate_round_participants_amount = evaluator.intermediate_round_participants_amount - if tournament.playoff_teams_amount != tournament.instant_finalists_amount + tournament.intermediate_round_participants_amount / 2 + if tournament.playoff_teams_amount != tournament.instant_finalists_amount + (tournament.intermediate_round_participants_amount / 2) raise 'playoff_teams_amount must be equal to instant_finalists_amount + intermediate_round_participants_amount / 2' end + tournament.save! end @@ -36,7 +33,7 @@ FactoryBot.define do (1..evaluator.stage_count).each do |level| tournament.stages << create( :playoff_stage, - level: level, + level:, match_count: -1, match_type: level == evaluator.stage_count ? :running_playoff_match : :empty_prepared_playoff_match ) diff --git a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb index 1b4f90b..a32e29c 100644 --- a/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb +++ b/spec/interactors/add_group_stage_to_tournament_interactor_spec.rb @@ -13,7 +13,7 @@ RSpec.describe AddGroupStageToTournament, type: :interactor do @empty_tournament = create(:stageless_tournament) @group_stage_tournament = create(:group_stage_tournament, stage_count: 0, group_count: 0) @group_stage = create(:group_stage) - @groups = Hash[1 => create_list(:team, 4), 2 => create_list(:team, 4)].values + @groups = { 1 => create_list(:team, 4), 2 => create_list(:team, 4) }.values @tournament_service_defaults = [78_345, 2_387] end diff --git a/spec/models/beamer_spec.rb b/spec/models/beamer_spec.rb index ba9681b..5dd4720 100644 --- a/spec/models/beamer_spec.rb +++ b/spec/models/beamer_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Beamer, type: :model do end it 'allows same name in different tournaments' do - beamer = create(:beamer, name: 'projector') + create(:beamer, name: 'projector') other = build(:beamer, name: 'projector', tournament: create(:tournament)) expect(other).to be_valid end diff --git a/spec/models/group_score_spec.rb b/spec/models/group_score_spec.rb index a2c61aa..422e810 100644 --- a/spec/models/group_score_spec.rb +++ b/spec/models/group_score_spec.rb @@ -15,21 +15,21 @@ RSpec.describe GroupScore, type: :model do let(:team_c) { create(:team) } let(:group_score_a) do - create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4) + create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4) end let(:group_score_b) do - create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 7, received_points: 4) + create(:group_score, group:, team: team_b, group_points: 6, scored_points: 7, received_points: 4) end it 'prefers the winner of the direct comparison when the first three criteria are tied' do - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 3) - create(:match_score, match: match, team: team_c, points: 1) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 3) + create(:match_score, match:, team: team_c, points: 1) end - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 4) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 4) + create(:match_score, match:, team: team_b, points: 2) end group_score_a.reload @@ -40,9 +40,9 @@ RSpec.describe GroupScore, type: :model do end it 'keeps the teams tied when their direct comparison was a draw' do - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end group_score_a.reload @@ -59,10 +59,10 @@ RSpec.describe GroupScore, type: :model do let(:team_b) { create(:team) } let(:group_score_a) do - create(:group_score, group: group, team: team_a, group_points: 6, scored_points: 7, received_points: 4) + create(:group_score, group:, team: team_a, group_points: 6, scored_points: 7, received_points: 4) end let(:group_score_b) do - create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 7, received_points: 4) + create(:group_score, group:, team: team_b, group_points: 3, scored_points: 7, received_points: 4) end it 'reports the first deciding metric when points are different' do @@ -78,7 +78,7 @@ RSpec.describe GroupScore, type: :model do it 'reports the direct comparison result when head-to-head resolves the tie' do group_score_b.update!(group_points: 6) - match = create(:group_match, group: group, state: :finished).tap do |created_match| + match = create(:group_match, group:, state: :finished).tap do |created_match| create(:match_score, match: created_match, team: team_a, points: 3) create(:match_score, match: created_match, team: team_b, points: 1) end @@ -95,7 +95,7 @@ RSpec.describe GroupScore, type: :model do it 'reports an unresolved tie when head-to-head was a draw' do group_score_b.update!(group_points: 6) - match = create(:group_match, group: group, state: :finished).tap do |created_match| + match = create(:group_match, group:, state: :finished).tap do |created_match| create(:match_score, match: created_match, team: team_a, points: 2) create(:match_score, match: created_match, team: team_b, points: 2) end @@ -112,11 +112,11 @@ RSpec.describe GroupScore, type: :model do it 'falls back to hidden points when direct comparison is still tied' do group_score_b.update!(group_points: 6) - direct_match = create(:group_match, group: group, state: :finished).tap do |created_match| + direct_match = create(:group_match, group:, state: :finished).tap do |created_match| create(:match_score, match: created_match, team: team_a, points: 2) create(:match_score, match: created_match, team: team_b, points: 2) end - decider_match = create(:group_match, group: group, state: :finished, decider: true).tap do |created_match| + decider_match = create(:group_match, group:, state: :finished, decider: true).tap do |created_match| create(:match_score, match: created_match, team: team_a, hidden_points: 3) create(:match_score, match: created_match, team: team_b, hidden_points: 1) end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 15c095d..2277b1c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -36,7 +36,7 @@ rescue ActiveRecord::PendingMigrationError => e end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_path = Rails.root.join('spec/fixtures').to_s # Run only focused tests # TODO REVERT ME diff --git a/spec/routing/tournaments_routing_spec.rb b/spec/routing/tournaments_routing_spec.rb index 8c30c0d..5cdb087 100644 --- a/spec/routing/tournaments_routing_spec.rb +++ b/spec/routing/tournaments_routing_spec.rb @@ -28,12 +28,12 @@ RSpec.describe TournamentsController, type: :routing do expect(delete: '/tournaments/1').to route_to('tournaments#destroy', id: '1') end - it 'routes to #sync_state' do - expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1') - end + it 'routes to #sync_state' do + expect(patch: '/tournaments/1/sync_state').to route_to('tournaments#sync_state', id: '1') + end - it 'routes to #test_sync' do - expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1') + it 'routes to #test_sync' do + expect(post: '/tournaments/1/test_sync').to route_to('tournaments#test_sync', id: '1') + end end end -end diff --git a/spec/services/group_stage_service_spec.rb b/spec/services/group_stage_service_spec.rb index 972009d..0b0a21e 100644 --- a/spec/services/group_stage_service_spec.rb +++ b/spec/services/group_stage_service_spec.rb @@ -4,7 +4,7 @@ RSpec.describe GroupStageService do before do @teams1 = create_list(:team, 4) @teams2 = create_list(:team, 4) - @prepared_groups = Hash[1 => @teams1, 2 => @teams2].values + @prepared_groups = { 1 => @teams1, 2 => @teams2 }.values end describe '#generate_group_stage' do let(:prepared_groups_groupstage) do @@ -52,7 +52,7 @@ RSpec.describe GroupStageService do end end - # TODO test get_advancing_teams when test data for running group stage is ready + # TODO: test get_advancing_teams when test data for running group stage is ready describe '#get_group_object_from' do it 'returns a group' do @@ -205,13 +205,13 @@ RSpec.describe GroupStageService do team_a = create(:team) team_b = create(:team) - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 4) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 4) + create(:match_score, match:, team: team_b, points: 2) end - create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2) - create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2) + create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2) + create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2) expect(GroupStageService.teams_sorted_by_group_scores(group.reload)).to eq([team_a, team_b]) end @@ -224,14 +224,14 @@ RSpec.describe GroupStageService do playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0, - group_stage: group_stage) + group_stage:) end let(:group_stage) { create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0) } let(:group) { create(:group, stage: group_stage, match_count: 0, number: 1) } - let(:team_a) { create(:team, tournament: tournament, name: 'A') } - let(:team_b) { create(:team, tournament: tournament, name: 'B') } - let(:team_c) { create(:team, tournament: tournament, name: 'C') } - let(:team_d) { create(:team, tournament: tournament, name: 'D') } + let(:team_a) { create(:team, tournament:, name: 'A') } + let(:team_b) { create(:team, tournament:, name: 'B') } + let(:team_c) { create(:team, tournament:, name: 'C') } + let(:team_d) { create(:team, tournament:, name: 'D') } before do group_stage.groups = [group] @@ -240,13 +240,13 @@ RSpec.describe GroupStageService do end it 'returns direct-comparison metadata for teams separated by head-to-head' do - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 4) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 4) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 3, scored_points: 4, received_points: 2) - score_b = create(:group_score, group: group, team: team_b, group_points: 3, scored_points: 4, received_points: 2) + score_a = create(:group_score, group:, team: team_a, group_points: 3, scored_points: 4, received_points: 2) + score_b = create(:group_score, group:, team: team_b, group_points: 3, scored_points: 4, received_points: 2) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!) @@ -269,17 +269,18 @@ RSpec.describe GroupStageService do end it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do - tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, + intermediate_round_participants_amount: 0) - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) - score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) - score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) + score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1) + score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3) + score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) @@ -301,17 +302,18 @@ RSpec.describe GroupStageService do end it 'requests a decider when tied teams both advance but their seeding stays unresolved' do - tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, + intermediate_round_participants_amount: 0) - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) - score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) - score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) + score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1) + score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3) + score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) @@ -333,17 +335,18 @@ RSpec.describe GroupStageService do end it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do - tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, + intermediate_round_participants_amount: 0) - create(:group_match, group: group, state: :in_progress).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :in_progress).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - score_a = create(:group_score, group: group, team: team_a, group_points: 1, scored_points: 2, received_points: 2) - score_b = create(:group_score, group: group, team: team_b, group_points: 1, scored_points: 2, received_points: 2) - score_c = create(:group_score, group: group, team: team_c, group_points: 0, scored_points: 0, received_points: 0) - score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 0, received_points: 0) + score_a = create(:group_score, group:, team: team_a, group_points: 1, scored_points: 2, received_points: 2) + score_b = create(:group_score, group:, team: team_b, group_points: 1, scored_points: 2, received_points: 2) + score_c = create(:group_score, group:, team: team_c, group_points: 0, scored_points: 0, received_points: 0) + score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 0, received_points: 0) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) @@ -352,12 +355,15 @@ RSpec.describe GroupStageService do end it 'does not expose unresolved tie metadata outside the advancing cutoff before the group is finished' do - tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, + intermediate_round_participants_amount: 0) - score_a = create(:group_score, group: group, team: team_a, group_points: 9, scored_points: 5, received_points: 1, position: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 6, scored_points: 4, received_points: 2, position: 2) - score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) - score_d = create(:group_score, group: group, team: team_d, group_points: 3, scored_points: 2, received_points: 3) + score_a = create(:group_score, group:, team: team_a, group_points: 9, scored_points: 5, received_points: 1, + position: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 6, scored_points: 4, received_points: 2, + position: 2) + score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3) + score_d = create(:group_score, group:, team: team_d, group_points: 3, scored_points: 2, received_points: 3) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) @@ -367,21 +373,22 @@ RSpec.describe GroupStageService do end it 'uses the decider hidden points to resolve a cutoff tie' do - tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0) + tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, + intermediate_round_participants_amount: 0) - create(:group_match, group: group, state: :finished).tap do |match| - create(:match_score, match: match, team: team_a, points: 2) - create(:match_score, match: match, team: team_b, points: 2) + create(:group_match, group:, state: :finished).tap do |match| + create(:match_score, match:, team: team_a, points: 2) + create(:match_score, match:, team: team_b, points: 2) end - create(:group_match, group: group, state: :finished, decider: true).tap do |match| - create(:match_score, match: match, team: team_a, hidden_points: 5) - create(:match_score, match: match, team: team_b, hidden_points: 3) + create(:group_match, group:, state: :finished, decider: true).tap do |match| + create(:match_score, match:, team: team_a, hidden_points: 5) + create(:match_score, match:, team: team_b, hidden_points: 3) end - score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1) - score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1) - score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3) - score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7) + score_a = create(:group_score, group:, team: team_a, group_points: 7, scored_points: 4, received_points: 1) + score_b = create(:group_score, group:, team: team_b, group_points: 7, scored_points: 4, received_points: 1) + score_c = create(:group_score, group:, team: team_c, group_points: 3, scored_points: 2, received_points: 3) + score_d = create(:group_score, group:, team: team_d, group_points: 0, scored_points: 1, received_points: 7) GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!) @@ -417,7 +424,7 @@ RSpec.describe GroupStageService do @tournament = create(:prepared_group_stage_tournament, group_stage: @group_stage, - teams: teams, + teams:, playoff_teams_amount: 16, instant_finalists_amount: 16, intermediate_round_participants_amount: 0) @@ -427,7 +434,7 @@ RSpec.describe GroupStageService do match.match_scores.each do |ms| # give the team 10 points minus the number in their name # this results in the team 0 always winning and getting to place 1 in the group etc. - ms.points = 10 - ms.team.name.split(' ').last.to_i + ms.points = 10 - ms.team.name.split.last.to_i ms.save! end match.state = 'finished' @@ -450,7 +457,7 @@ RSpec.describe GroupStageService do advancing_teams.each_with_index do |team, i| # if index is even, the team should be of a first place; end in a 0 # if index is odd, the team should be of a second place; end in a 1 - team_quality = team.name.split(' ').last.to_i + team_quality = team.name.split.last.to_i expect(team_quality % 2).to be(i % 2) end end @@ -469,9 +476,9 @@ RSpec.describe GroupStageService do advancing_teams = GroupStageService.get_advancing_teams(@group_stage) advancing_teams.each_slice(2).to_a.each do |matchup| # this is the team that landed a first place in the group - first_place_team = matchup[0].name.split(' ')[1].to_i + first_place_team = matchup[0].name.split[1].to_i # this is the team that landed a second place in the group - second_place_team = matchup[1].name.split(' ')[1].to_i + second_place_team = matchup[1].name.split[1].to_i expect(group_first_matchups_expected[first_place_team]).to eq(second_place_team) end end diff --git a/spec/services/match_service_spec.rb b/spec/services/match_service_spec.rb index 20dc4b2..4dc1b18 100644 --- a/spec/services/match_service_spec.rb +++ b/spec/services/match_service_spec.rb @@ -62,7 +62,7 @@ RSpec.describe MatchService do first_team = match.match_scores.first.team.name second_team = match.match_scores.second.team.name expect(first_team).to eq(teams[2 * index].name) - expect(second_team).to eq(teams[2 * index + 1].name) + expect(second_team).to eq(teams[(2 * index) + 1].name) end end end diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 351e5fa..c2197d2 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -61,7 +61,7 @@ RSpec.describe PlayoffStageService do { team_size: 9, expected_amount_of_playoff_stages: 4 }, { team_size: 10, expected_amount_of_playoff_stages: 4 }, { team_size: 16, expected_amount_of_playoff_stages: 4 }, - { team_size: 24, expected_amount_of_playoff_stages: 5 }, + { team_size: 24, expected_amount_of_playoff_stages: 5 } ].each do |parameters| it "generates playoff stages for #{parameters[:team_size]} teams" do amount_of_teams = parameters[:team_size] diff --git a/spec/services/team_action_list_service_spec.rb b/spec/services/team_action_list_service_spec.rb index f71d4a1..22328c6 100644 --- a/spec/services/team_action_list_service_spec.rb +++ b/spec/services/team_action_list_service_spec.rb @@ -48,7 +48,8 @@ RSpec.describe TeamActionListService do action_name: 'Break', source: 'unknown_source' ) - end.to raise_error(TeamActionListService::InvalidSourceError, 'Unsupported team action list source: unknown_source') + end.to raise_error(TeamActionListService::InvalidSourceError, + 'Unsupported team action list source: unknown_source') end it 'raises when source resolves to no teams' do @@ -62,7 +63,8 @@ RSpec.describe TeamActionListService do action_name: 'Nothing', source: 'all_tournament_teams' ) - end.to raise_error(TeamActionListService::EmptySourceError, 'Source all_tournament_teams does not currently contain any teams') + end.to raise_error(TeamActionListService::EmptySourceError, + 'Source all_tournament_teams does not currently contain any teams') end end end diff --git a/spec/services/tournament_live_payload_spec.rb b/spec/services/tournament_live_payload_spec.rb index 3d3502e..3725ed5 100644 --- a/spec/services/tournament_live_payload_spec.rb +++ b/spec/services/tournament_live_payload_spec.rb @@ -31,7 +31,7 @@ RSpec.describe 'Tournament live payloads' do expect(serialized_match[:position]).to eq(match.position) expect(serialized_match.dig(:group, :id)).to eq(match.group_id) expect(serialized_match.dig(:stage, :id)).to eq(match.group.stage_id) - expect(serialized_match[:teams].map { |team| team[:id] }).to match_array(match.teams.map(&:id)) + expect(serialized_match[:teams].pluck(:id)).to match_array(match.teams.map(&:id)) expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id)) end end diff --git a/spec/services/tournament_snapshot_importer_spec.rb b/spec/services/tournament_snapshot_importer_spec.rb index 29292ee..52ddd34 100644 --- a/spec/services/tournament_snapshot_importer_spec.rb +++ b/spec/services/tournament_snapshot_importer_spec.rb @@ -13,7 +13,7 @@ RSpec.describe TournamentSnapshotImporter do snapshot = TournamentSnapshotBuilder.build(source) - described_class.import!(tournament: follower, snapshot: snapshot) + described_class.import!(tournament: follower, snapshot:) follower.reload expect(follower.sync_source_tournament_id).to eq(source.id) @@ -25,8 +25,12 @@ RSpec.describe TournamentSnapshotImporter do expect(follower.team_action_lists.first.team_action_items.count).to eq(snapshot.fetch(:team_action_items).count) expect(follower.stages.count).to eq(source.stages.count) expect(follower.matches.count).to eq(source.matches.count) - expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map { |team| team.fetch(:id) }) - expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map { |beamer| beamer.fetch(:id) }) + expect(follower.teams.pluck(:sync_source_id)).to match_array(snapshot.fetch(:teams).map do |team| + team.fetch(:id) + end) + expect(follower.beamers.pluck(:sync_source_id)).to match_array(snapshot.fetch(:beamers).map do |beamer| + beamer.fetch(:id) + end) end it 'ignores stale snapshots that arrive after a newer one' do @@ -54,7 +58,7 @@ RSpec.describe TournamentSnapshotImporter do snapshot = TournamentSnapshotBuilder.build(source) - described_class.import!(tournament: follower, snapshot: snapshot) + described_class.import!(tournament: follower, snapshot:) follower.reload expect(follower.id).not_to eq(source.id) @@ -62,7 +66,7 @@ RSpec.describe TournamentSnapshotImporter do expect(follower.teams.count).to eq(128) expect(follower.stages.where(level: -1).first.groups.count).to eq(32) expect(follower.stages.where('level >= 0').sum { |stage| stage.matches.count }).to eq(64) - expect(follower.matches.select(&:decider?).count).to eq(source.matches.select(&:decider?).count) + expect(follower.matches.count(&:decider?)).to eq(source.matches.count(&:decider?)) expect(follower.teams.pluck(:sync_source_id)).to match_array(source.teams.pluck(:id)) expect(source.teams.pluck(:id) & follower.teams.pluck(:id)).not_to match_array(source.teams.pluck(:id)) end @@ -95,15 +99,15 @@ RSpec.describe TournamentSnapshotImporter do logo_url: '/logos/stream-b.png' ) - group_stage = Stage.create!(tournament: tournament, level: -1, state: :finished) + group_stage = Stage.create!(tournament:, level: -1, state: :finished) advancing_teams = [] 32.times do |group_index| group = Group.create!(stage: group_stage, number: group_index + 1) - teams = 4.times.map do |seed_index| + teams = Array.new(4) do |seed_index| create( :team, - tournament: tournament, + tournament:, name: "Leader-G#{group_index + 1}-S#{seed_index + 1}" ) end @@ -111,17 +115,15 @@ RSpec.describe TournamentSnapshotImporter do teams[0].update!(name: "#{teams[0].name} Renamed") if group_index.zero? teams[1].update!(name: "#{teams[1].name} Renamed") if group_index == 15 - create_group_round_robin!(group: group, teams: teams) - create_group_scores!(group: group, teams: teams) + create_group_round_robin!(group:, teams:) + create_group_scores!(group:, teams:) - if group_index < 3 - create_decider_match!(group: group, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) - end + create_decider_match!(group:, teams: teams.take(2), hidden_points: [8 - group_index, 4 + group_index]) if group_index < 3 advancing_teams.concat(teams.take(2)) end - result = AddPlayoffsToTournamentAndSave.call(tournament: tournament, teams: advancing_teams) + result = AddPlayoffsToTournamentAndSave.call(tournament:, teams: advancing_teams) expect(result).to be_a_success tournament.reload @@ -140,23 +142,23 @@ RSpec.describe TournamentSnapshotImporter do ] pairings.each_with_index do |(left_index, right_index, left_points, right_points), position| - match = Match.create!(group: group, state: :finished, position: position, decider: false) - MatchScore.create!(match: match, team: teams[left_index], points: left_points, hidden_points: 0) - MatchScore.create!(match: match, team: teams[right_index], points: right_points, hidden_points: 0) + match = Match.create!(group:, state: :finished, position:, decider: false) + MatchScore.create!(match:, team: teams[left_index], points: left_points, hidden_points: 0) + MatchScore.create!(match:, team: teams[right_index], points: right_points, hidden_points: 0) end end def create_decider_match!(group:, teams:, hidden_points:) - match = Match.create!(group: group, state: :finished, position: 6, decider: true) - MatchScore.create!(match: match, team: teams.first, points: 0, hidden_points: hidden_points.first) - MatchScore.create!(match: match, team: teams.second, points: 0, hidden_points: hidden_points.last) + match = Match.create!(group:, state: :finished, position: 6, decider: true) + MatchScore.create!(match:, team: teams.first, points: 0, hidden_points: hidden_points.first) + MatchScore.create!(match:, team: teams.second, points: 0, hidden_points: hidden_points.last) end def create_group_scores!(group:, teams:) teams.each_with_index do |team, index| GroupScore.create!( - group: group, - team: team, + group:, + team:, group_points: 9 - index, scored_points: 12 - index, received_points: index, @@ -185,7 +187,7 @@ RSpec.describe TournamentSnapshotImporter do intermediate_round_participants_amount: tournament.intermediate_round_participants_amount, timestamp: tournament.timestamp&.iso8601, timer_mode: tournament.timer_mode, - teams: tournament.teams.order(:name).map { |team| team.name }, + teams: tournament.teams.order(:name).map(&:name), team_action_lists: tournament.team_action_lists.order(:key).map do |list| { key: list.key, diff --git a/spec/services/tournament_sync_enqueue_spec.rb b/spec/services/tournament_sync_enqueue_spec.rb index 77cc3ad..7b9d103 100644 --- a/spec/services/tournament_sync_enqueue_spec.rb +++ b/spec/services/tournament_sync_enqueue_spec.rb @@ -9,7 +9,7 @@ RSpec.describe TournamentSyncEnqueue do sync_target_url: 'https://remote.example.com/tournaments/1/sync_state', sync_auth_token: 'shared-secret') entry = TournamentSyncQueueEntry.create!( - tournament: tournament, + tournament:, snapshot_json: '{"old":true}', status: 'pending', attempts: 4, diff --git a/spec/services/tournament_sync_processor_spec.rb b/spec/services/tournament_sync_processor_spec.rb index f3b54ec..94445c6 100644 --- a/spec/services/tournament_sync_processor_spec.rb +++ b/spec/services/tournament_sync_processor_spec.rb @@ -9,9 +9,10 @@ RSpec.describe TournamentSyncProcessor do sync_target_url: 'https://remote.example.com/tournaments/1/sync_state', sync_auth_token: 'shared-secret') TournamentSyncEnqueue.call(tournament) - entry = TournamentSyncQueueEntry.find_by!(tournament: tournament) + entry = TournamentSyncQueueEntry.find_by!(tournament:) - allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed, 'network down') + allow(TournamentSyncPusher).to receive(:push_snapshot!).and_raise(TournamentSyncPusher::SyncFailed, + 'network down') described_class.process_due! diff --git a/spec/support/performance_helpers.rb b/spec/support/performance_helpers.rb index 44b22a3..161b81c 100644 --- a/spec/support/performance_helpers.rb +++ b/spec/support/performance_helpers.rb @@ -22,7 +22,7 @@ module PerformanceHelpers end duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 - { result: result, query_count: query_count, duration_ms: duration_ms } + { result:, query_count:, duration_ms: } end def create_group_stage_tournament(groups_count:, teams_per_group:) diff --git a/spec/support/profile.rb b/spec/support/profile.rb index f2a9f48..c3ed499 100644 --- a/spec/support/profile.rb +++ b/spec/support/profile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'ruby-prof' RSpec.configure do |config| @@ -10,4 +12,4 @@ RSpec.configure do |config| printer = RubyProf::FlatPrinter.new(result) printer.print(File.open("profile_#{example.full_description.parameterize}.txt", 'w+')) end -end \ No newline at end of file +end diff --git a/tasks.py b/tasks.py index a1b8ac3..40adca3 100644 --- a/tasks.py +++ b/tasks.py @@ -670,7 +670,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False): @task def lint(c): """Run RuboCop.""" - c.run(_bundle_exec("rubocop"), pty=True) + c.run(_bundle_exec("rubocop", "--cache", "false"), pty=True) @task(name="bootstrap-dev-user") From 53cb6e38bd03fbb5b0d8c36c1c63f3c521545a96 Mon Sep 17 00:00:00 2001 From: Malaber Date: Wed, 6 May 2026 12:39:12 +0200 Subject: [PATCH 2/5] fix: align CI RuboCop and e2e helpers --- .rubocop.yml | 5 +++++ Dockerfile | 1 + e2e/lib/action_cable_client.rb | 16 +++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0c74afb..fc13361 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,6 +66,11 @@ Rails/SkipsModelValidations: - "app/services/tournament_sync_pusher.rb" - "db/migrate/**/*" +Rails/Delegate: + Exclude: + - "app/controllers/tournaments_controller.rb" + - "e2e/lib/action_cable_client.rb" + Style/MultilineBlockChain: Exclude: - "e2e/lib/api_client.rb" diff --git a/Dockerfile b/Dockerfile index e7c10d1..ec73507 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,7 @@ COPY script /app/script COPY config.ru Rakefile /app/ COPY config/environments/production.rb /app/config/environments/beta.rb COPY tasks.py /app/tasks.py +COPY .rubocop.yml /app/.rubocop.yml COPY e2e /app/e2e COPY lib /app/lib COPY spec /app/spec diff --git a/e2e/lib/action_cable_client.rb b/e2e/lib/action_cable_client.rb index f6dd428..f3d9f80 100644 --- a/e2e/lib/action_cable_client.rb +++ b/e2e/lib/action_cable_client.rb @@ -121,7 +121,9 @@ module TurniereE2E websocket_url end - delegate :write, to: :socket + def write(data) + socket.write(data) + end private @@ -182,8 +184,8 @@ module TurniereE2E end def pump_until_open!(timeout: 10) - deadline = Time.zone.now + timeout - while Time.zone.now < deadline + deadline = monotonic_deadline(timeout) + while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline read_from_socket(timeout: 0.5) return if driver.ready_state == 1 end @@ -192,14 +194,14 @@ module TurniereE2E end def wait_for!(timeout: 20) - deadline = Time.zone.now + timeout + deadline = monotonic_deadline(timeout) loop do until @messages.empty? message = @messages.pop(true) return message if yield(message) end - raise "websocket message not received within #{timeout}s" if Time.zone.now >= deadline + raise "websocket message not received within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline read_from_socket(timeout: 0.5) rescue ThreadError @@ -207,6 +209,10 @@ module TurniereE2E end end + def monotonic_deadline(timeout) + Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout + end + def read_from_socket(timeout:) readable, = IO.select([socket], nil, nil, timeout) return if readable.nil? From e53a9e7eb0a1689926e0bc31c89c4ae74c464adf Mon Sep 17 00:00:00 2001 From: Malaber Date: Wed, 6 May 2026 14:42:32 +0200 Subject: [PATCH 3/5] build: include RuboCop config in image --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 6672e96..cee8f5f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ db/*.sqlite3.* docker/** .gitignore .hound.yml -.rubocop.yml docker-compose.yml README.md .gitlab-ci.yml From d847fa694d1c9b8c1333bbf9dd0c4f653b7d9892 Mon Sep 17 00:00:00 2001 From: Malaber Date: Wed, 6 May 2026 14:47:33 +0200 Subject: [PATCH 4/5] fix: clean e2e script lint --- e2e/load_test.rb | 55 ++++++++++++++++++++++------------------- script/e2e_scenarios.rb | 17 ++++++++++--- script/wait_for_http.rb | 2 +- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/e2e/load_test.rb b/e2e/load_test.rb index 1ca9e07..c32400a 100755 --- a/e2e/load_test.rb +++ b/e2e/load_test.rb @@ -6,6 +6,10 @@ require 'optparse' require_relative 'lib/load_test_runner' +def write_line(message = '') + $stdout.write("#{message}\n") +end + def parse_client_list(value) return [] if value.nil? || value.strip == '' || value.strip.casecmp('none').zero? @@ -72,33 +76,32 @@ runner = TurniereE2E::LoadTestRunner.new(**runner_options) result = runner.run if options[:format] == 'json' - puts JSON.pretty_generate(result) - exit -end + write_line JSON.pretty_generate(result) +else + write_line "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" -puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" + result.fetch(:http).each do |level| + write_line( + "HTTP #{level.fetch(:clients)} clients: " \ + "#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \ + "p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \ + "max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}" + ) + end -result.fetch(:http).each do |level| - puts( - "HTTP #{level.fetch(:clients)} clients: " \ - "#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \ - "p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \ - "max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}" + result.fetch(:websocket).each do |level| + write_line( + "WS #{level.fetch(:clients)} clients: " \ + "opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \ + "p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \ + "connections_metric=#{level.fetch(:connection_metric)}" + ) + end + + metrics = result.fetch(:metrics) + write_line( + "Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \ + "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ + "ruby_threads=#{metrics.fetch(:ruby_threads)}" ) end - -result.fetch(:websocket).each do |level| - puts( - "WS #{level.fetch(:clients)} clients: " \ - "opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \ - "p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \ - "connections_metric=#{level.fetch(:connection_metric)}" - ) -end - -metrics = result.fetch(:metrics) -puts( - "Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \ - "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ - "ruby_threads=#{metrics.fetch(:ruby_threads)}" -) diff --git a/script/e2e_scenarios.rb b/script/e2e_scenarios.rb index 17d6741..238ea05 100755 --- a/script/e2e_scenarios.rb +++ b/script/e2e_scenarios.rb @@ -5,6 +5,15 @@ require 'json' require 'optparse' require_relative '../e2e/lib/scenario_runner' +def write_line(message) + $stdout.write("#{message}\n") +end + +def fail_with(message) + $stderr.write("#{message}\n") + raise SystemExit, 1 +end + options = { base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'), stop_at: nil, @@ -39,7 +48,9 @@ OptionParser.new do |parser| parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value } end.parse! -scenario = ARGV.shift or abort('Missing scenario name') +scenario = ARGV.shift +fail_with('Missing scenario name') if scenario.nil? + runner = TurniereE2E::ScenarioRunner.new( base_url: options[:base_url], email: options[:email], @@ -64,7 +75,7 @@ result = case scenario stop_at: options[:stop_at] || :profiled_show ) else - abort("Unknown scenario #{scenario.inspect}") + fail_with("Unknown scenario #{scenario.inspect}") end -puts JSON.pretty_generate(result) +write_line JSON.pretty_generate(result) diff --git a/script/wait_for_http.rb b/script/wait_for_http.rb index 1513de2..dad5f0d 100755 --- a/script/wait_for_http.rb +++ b/script/wait_for_http.rb @@ -8,4 +8,4 @@ timeout = (ARGV[1] || 30).to_i runner = TurniereE2E::ScenarioRunner.new(base_url: url) runner.wait_for_healthcheck!(timeout_seconds: timeout) -puts "ready: #{url}" +$stdout.write("ready: #{url}\n") From d8e354ec1b20b8626d4b96eaa5ae469199ceed26 Mon Sep 17 00:00:00 2001 From: Malaber Date: Wed, 6 May 2026 15:21:37 +0200 Subject: [PATCH 5/5] fix: allow CLI script output lint --- .rubocop.yml | 8 ++++++++ e2e/load_test.rb | 14 +++++--------- script/e2e_scenarios.rb | 8 ++------ script/wait_for_http.rb | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fc13361..8981d87 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,6 +66,14 @@ Rails/SkipsModelValidations: - "app/services/tournament_sync_pusher.rb" - "db/migrate/**/*" +# These are standalone CLI entrypoints where stdout is command output, +# not Rails application logging. +Rails/Output: + Exclude: + - "e2e/load_test.rb" + - "script/e2e_scenarios.rb" + - "script/wait_for_http.rb" + Rails/Delegate: Exclude: - "app/controllers/tournaments_controller.rb" diff --git a/e2e/load_test.rb b/e2e/load_test.rb index c32400a..def4057 100755 --- a/e2e/load_test.rb +++ b/e2e/load_test.rb @@ -6,10 +6,6 @@ require 'optparse' require_relative 'lib/load_test_runner' -def write_line(message = '') - $stdout.write("#{message}\n") -end - def parse_client_list(value) return [] if value.nil? || value.strip == '' || value.strip.casecmp('none').zero? @@ -76,12 +72,12 @@ runner = TurniereE2E::LoadTestRunner.new(**runner_options) result = runner.run if options[:format] == 'json' - write_line JSON.pretty_generate(result) + puts JSON.pretty_generate(result) else - write_line "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" + puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" result.fetch(:http).each do |level| - write_line( + puts( "HTTP #{level.fetch(:clients)} clients: " \ "#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \ "p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \ @@ -90,7 +86,7 @@ else end result.fetch(:websocket).each do |level| - write_line( + puts( "WS #{level.fetch(:clients)} clients: " \ "opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \ "p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \ @@ -99,7 +95,7 @@ else end metrics = result.fetch(:metrics) - write_line( + puts( "Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \ "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ "ruby_threads=#{metrics.fetch(:ruby_threads)}" diff --git a/script/e2e_scenarios.rb b/script/e2e_scenarios.rb index 238ea05..c35102b 100755 --- a/script/e2e_scenarios.rb +++ b/script/e2e_scenarios.rb @@ -5,12 +5,8 @@ require 'json' require 'optparse' require_relative '../e2e/lib/scenario_runner' -def write_line(message) - $stdout.write("#{message}\n") -end - def fail_with(message) - $stderr.write("#{message}\n") + warn message raise SystemExit, 1 end @@ -78,4 +74,4 @@ result = case scenario fail_with("Unknown scenario #{scenario.inspect}") end -write_line JSON.pretty_generate(result) +puts JSON.pretty_generate(result) diff --git a/script/wait_for_http.rb b/script/wait_for_http.rb index dad5f0d..1513de2 100755 --- a/script/wait_for_http.rb +++ b/script/wait_for_http.rb @@ -8,4 +8,4 @@ timeout = (ARGV[1] || 30).to_i runner = TurniereE2E::ScenarioRunner.new(base_url: url) runner.wait_for_healthcheck!(timeout_seconds: timeout) -$stdout.write("ready: #{url}\n") +puts "ready: #{url}"