Merge remote-tracking branch 'origin/master' into codex/TUR-113-fix-rubocop
# Conflicts: # spec/e2e/http/api_surface_spec.rb # spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb
This commit is contained in:
commit
6db6442205
|
|
@ -54,7 +54,10 @@ RUN bundle config set with 'development test' \
|
||||||
|
|
||||||
FROM runtime-base AS production
|
FROM runtime-base AS production
|
||||||
|
|
||||||
|
ARG GIT_COMMIT_SHA=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
|
ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA}
|
||||||
|
|
||||||
COPY --from=bundle-production /usr/local/bundle /usr/local/bundle
|
COPY --from=bundle-production /usr/local/bundle /usr/local/bundle
|
||||||
COPY --from=bundle-production /app/vendor/bundle /app/vendor/bundle
|
COPY --from=bundle-production /app/vendor/bundle /app/vendor/bundle
|
||||||
|
|
@ -75,7 +78,10 @@ CMD ["sh", "-lc", "bundle exec rails db:migrate && bundle exec rails s -p 3000 -
|
||||||
|
|
||||||
FROM runtime-base AS test
|
FROM runtime-base AS test
|
||||||
|
|
||||||
|
ARG GIT_COMMIT_SHA=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
|
||||||
ENV RAILS_ENV=test
|
ENV RAILS_ENV=test
|
||||||
|
ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA}
|
||||||
|
|
||||||
RUN apt-get update -qq \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,28 @@
|
||||||
|
|
||||||
class TournamentMatchesChannel < ApplicationCable::Channel
|
class TournamentMatchesChannel < ApplicationCable::Channel
|
||||||
def subscribed
|
def subscribed
|
||||||
tournament = Tournament.find(params[:tournament_id])
|
@tournament_id = params[:tournament_id]
|
||||||
state = params[:state].presence
|
@filters = match_filters
|
||||||
|
tournament = Tournament.find(@tournament_id)
|
||||||
|
|
||||||
stream_for TournamentMatchesBroadcast.stream_target(tournament, state:)
|
stream_for TournamentMatchesBroadcast.stream_target(tournament) do
|
||||||
transmit TournamentMatchesBroadcastPayload.build(tournament, state:)
|
transmit_current_payload
|
||||||
|
end
|
||||||
|
transmit TournamentMatchesBroadcastPayload.build(tournament, @filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :tournament_id, :filters
|
||||||
|
|
||||||
|
def match_filters
|
||||||
|
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position)
|
||||||
|
end
|
||||||
|
|
||||||
|
def transmit_current_payload
|
||||||
|
tournament = TournamentMatchesBroadcast.load_tournament(tournament_id)
|
||||||
|
return if tournament.nil?
|
||||||
|
|
||||||
|
transmit TournamentMatchesBroadcastPayload.build(tournament, filters)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,12 @@ class BeamersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def beamer_params
|
def beamer_params
|
||||||
params.permit(:name, :display_state, :qr_code_url, :logo_url)
|
permitted = params.permit(:name, :display_state).to_h
|
||||||
|
config = params.key?(:config) ? params[:config] : params.dig(:beamer, :config)
|
||||||
|
return permitted unless config
|
||||||
|
|
||||||
|
permitted[:config] = config.respond_to?(:to_unsafe_h) ? config.to_unsafe_h : config
|
||||||
|
permitted
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_sync_if_needed!
|
def push_sync_if_needed!
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class MatchesController < ApplicationController
|
||||||
|
|
||||||
# GET/tournaments/1/matches
|
# GET/tournaments/1/matches
|
||||||
def index
|
def index
|
||||||
render json: TournamentMatchesPayload.build(@tournament, state: match_params['state'])
|
render json: TournamentMatchesPayload.build(@tournament, match_index_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /matches/1
|
# GET /matches/1
|
||||||
|
|
@ -145,6 +145,10 @@ class MatchesController < ApplicationController
|
||||||
params.slice(:state).permit!
|
params.slice(:state).permit!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_index_params
|
||||||
|
params.slice(:state, :stage_id, :stage_level, :position, :group_stage_position).permit!
|
||||||
|
end
|
||||||
|
|
||||||
def create_match_params
|
def create_match_params
|
||||||
params.slice(:team_ids).permit(team_ids: [])
|
params.slice(:team_ids).permit(team_ids: [])
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VersionController < ApplicationController
|
||||||
|
def show
|
||||||
|
render json: { git_commit_sha: }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def git_commit_sha
|
||||||
|
ENV['GIT_COMMIT_SHA'].presence || local_git_commit_sha || 'unknown'
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_git_commit_sha
|
||||||
|
Rails.root.join('.git').exist? ? `git rev-parse HEAD`.strip.presence : nil
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -2,15 +2,16 @@
|
||||||
|
|
||||||
class Beamer < ApplicationRecord
|
class Beamer < ApplicationRecord
|
||||||
DEFAULT_NAME = 'default'
|
DEFAULT_NAME = 'default'
|
||||||
|
DEFAULT_CONFIG = { 'match_filter' => 'state:in_progress' }.freeze
|
||||||
DISPLAY_STATES = %w[
|
DISPLAY_STATES = %w[
|
||||||
matches_all matches_in_progress matches_upcoming matches_not_started
|
during_game between_games static_image group_tables
|
||||||
matches_finished groups between_games during_game
|
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
belongs_to :tournament
|
belongs_to :tournament
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: { scope: :tournament_id }
|
validates :name, presence: true, uniqueness: { scope: :tournament_id }
|
||||||
validates :display_state, presence: true, inclusion: { in: DISPLAY_STATES }
|
validates :display_state, presence: true, inclusion: { in: DISPLAY_STATES }
|
||||||
|
validate :config_is_hash
|
||||||
|
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
|
|
@ -46,4 +47,10 @@ class Beamer < ApplicationRecord
|
||||||
|
|
||||||
previous_changes.key?('id') ? 'created' : 'updated'
|
previous_changes.key?('id') ? 'created' : 'updated'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def config_is_hash
|
||||||
|
return if config.is_a?(Hash)
|
||||||
|
|
||||||
|
errors.add(:config, 'must be an object')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,29 @@ class Tournament < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_configuration_is_complete
|
def sync_configuration_is_complete
|
||||||
return if sync_target_url.blank? && sync_auth_token.blank?
|
return if sync_configuration_blank?
|
||||||
return if sync_target_url.present? && sync_auth_token.present?
|
return if sync_configuration_complete?
|
||||||
return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank?
|
return if follower_sync_token_only?
|
||||||
|
|
||||||
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
|
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync_configuration_blank?
|
||||||
|
sync_target_url.blank? && sync_auth_token.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_configuration_complete?
|
||||||
|
sync_target_url.present? && sync_auth_token.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def follower_sync_token_only?
|
||||||
|
read_only_mode? && sync_auth_token.present? && sync_target_url.blank?
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_default_beamer!
|
def ensure_default_beamer!
|
||||||
beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer|
|
beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer|
|
||||||
beamer.display_state = 'matches_all'
|
beamer.display_state = 'during_game'
|
||||||
|
beamer.config = Beamer::DEFAULT_CONFIG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class BeamerSerializer < ApplicationSerializer
|
class BeamerSerializer < ApplicationSerializer
|
||||||
attributes :id, :name, :display_state, :qr_code_url, :logo_url
|
attributes :id, :name, :display_state, :config
|
||||||
|
|
||||||
|
delegate :config, to: :object
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ class BeamerBroadcastPayload
|
||||||
tournament_id: beamer.tournament_id,
|
tournament_id: beamer.tournament_id,
|
||||||
name: beamer.name,
|
name: beamer.name,
|
||||||
display_state: beamer.display_state,
|
display_state: beamer.display_state,
|
||||||
qr_code_url: beamer.qr_code_url,
|
config: beamer.config
|
||||||
logo_url: beamer.logo_url
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -31,8 +30,7 @@ class BeamerBroadcastPayload
|
||||||
id: snapshot.fetch(:id),
|
id: snapshot.fetch(:id),
|
||||||
name: snapshot.fetch(:name),
|
name: snapshot.fetch(:name),
|
||||||
display_state: snapshot.fetch(:display_state),
|
display_state: snapshot.fetch(:display_state),
|
||||||
qr_code_url: snapshot[:qr_code_url],
|
config: snapshot.fetch(:config)
|
||||||
logo_url: snapshot[:logo_url]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,21 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TournamentMatchesBroadcast
|
class TournamentMatchesBroadcast
|
||||||
STATES = [nil, 'upcoming', *Match.states.keys].freeze
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def call(tournament_id)
|
def call(tournament_id)
|
||||||
tournament = load_tournament(tournament_id)
|
tournament = load_tournament(tournament_id)
|
||||||
return if tournament.nil?
|
return if tournament.nil?
|
||||||
|
|
||||||
STATES.each do |state|
|
TournamentMatchesChannel.broadcast_to(
|
||||||
TournamentMatchesChannel.broadcast_to(
|
stream_target(tournament),
|
||||||
stream_target(tournament, state:),
|
{ type: 'tournament_matches.changed', tournament_id: tournament.id }
|
||||||
TournamentMatchesBroadcastPayload.build(tournament, state:)
|
)
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def stream_target(tournament, state:)
|
def stream_target(tournament)
|
||||||
"#{tournament.to_gid_param}:matches:#{state.presence || 'all'}"
|
"#{tournament.to_gid_param}:matches"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def load_tournament(tournament_id)
|
def load_tournament(tournament_id)
|
||||||
Tournament.includes(
|
Tournament.includes(
|
||||||
stages: [
|
stages: [
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
class TournamentMatchesBroadcastPayload
|
class TournamentMatchesBroadcastPayload
|
||||||
class << self
|
class << self
|
||||||
def build(tournament, state: nil)
|
def build(tournament, filters = {}, state: nil)
|
||||||
|
filters = TournamentMatchesPayload.normalize_filters(filters, state:)
|
||||||
{
|
{
|
||||||
type: 'tournament_matches.updated',
|
type: 'tournament_matches.updated',
|
||||||
tournament_id: tournament.id,
|
tournament_id: tournament.id,
|
||||||
state:,
|
state: filters[:state],
|
||||||
matches: TournamentMatchesPayload.build(tournament, state:)
|
matches: TournamentMatchesPayload.build(tournament, filters)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,77 @@
|
||||||
|
|
||||||
class TournamentMatchesPayload
|
class TournamentMatchesPayload
|
||||||
class << self
|
class << self
|
||||||
def build(tournament, state: nil)
|
def build(tournament, filters = {}, state: nil, **filter_keywords)
|
||||||
serialized_matches(matches_for(tournament, state:))
|
serialized_matches(matches_for(tournament, filters.merge(filter_keywords), state:))
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches_for(tournament, state: nil)
|
def matches_for(tournament, filters = {}, state: nil, **filter_keywords)
|
||||||
|
filters = normalize_filters(filters.merge(filter_keywords), state:)
|
||||||
|
matches = state_filtered_matches(tournament, filters[:state])
|
||||||
|
|
||||||
|
matches.select { |match| match_matches_filters?(match, filters) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_filters(filters = {}, state: nil)
|
||||||
|
raw_filters = filters.to_h.with_indifferent_access
|
||||||
|
raw_filters[:state] = state unless state.nil?
|
||||||
|
|
||||||
|
{
|
||||||
|
state: raw_filters[:state].presence,
|
||||||
|
stage_id: integer_filter(raw_filters[:stage_id]),
|
||||||
|
stage_level: integer_filter(raw_filters[:stage_level]),
|
||||||
|
position: integer_filter(raw_filters[:position]),
|
||||||
|
group_stage_position: integer_filter(raw_filters[:group_stage_position])
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state_filtered_matches(tournament, state)
|
||||||
return tournament.matches if state.blank?
|
return tournament.matches if state.blank?
|
||||||
return upcoming_matches_for(tournament) if state == 'upcoming'
|
return upcoming_matches_for(tournament) if state == 'upcoming'
|
||||||
|
|
||||||
tournament.matches.select { |match| match.state == state }
|
tournament.matches.select { |match| match.state == state }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def match_matches_filters?(match, filters)
|
||||||
|
stage = match_stage(match)
|
||||||
|
|
||||||
|
stage_id_matches?(stage, filters) &&
|
||||||
|
stage_level_matches?(stage, filters) &&
|
||||||
|
position_matches?(match, filters) &&
|
||||||
|
group_stage_position_matches?(match, stage, filters)
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_stage(match)
|
||||||
|
match.stage || match.group&.stage
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_id_matches?(stage, filters)
|
||||||
|
filters[:stage_id].nil? || stage&.id == filters[:stage_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_level_matches?(stage, filters)
|
||||||
|
filters[:stage_level].nil? || stage&.level == filters[:stage_level]
|
||||||
|
end
|
||||||
|
|
||||||
|
def position_matches?(match, filters)
|
||||||
|
filters[:position].nil? || match.position == filters[:position]
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_stage_position_matches?(match, stage, filters)
|
||||||
|
filters[:group_stage_position].nil? ||
|
||||||
|
(match.group_match? && stage&.level == -1 && match.position == filters[:group_stage_position])
|
||||||
|
end
|
||||||
|
|
||||||
|
def integer_filter(value)
|
||||||
|
return nil if value.blank?
|
||||||
|
return value if value.is_a?(Integer)
|
||||||
|
|
||||||
|
Integer(value, 10)
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
:invalid
|
||||||
|
end
|
||||||
|
|
||||||
def upcoming_matches_for(tournament)
|
def upcoming_matches_for(tournament)
|
||||||
upcoming_group_matches(tournament).presence || upcoming_playoff_matches(tournament)
|
upcoming_group_matches(tournament).presence || upcoming_playoff_matches(tournament)
|
||||||
|
|
|
||||||
|
|
@ -109,8 +109,7 @@ class TournamentSnapshotImporter
|
||||||
beamer.name = attrs.fetch(:name)
|
beamer.name = attrs.fetch(:name)
|
||||||
beamer.display_state = attrs.fetch(:display_state)
|
beamer.display_state = attrs.fetch(:display_state)
|
||||||
beamer.is_default = attrs.fetch(:is_default, false)
|
beamer.is_default = attrs.fetch(:is_default, false)
|
||||||
beamer.qr_code_url = attrs[:qr_code_url]
|
beamer.config = attrs.fetch(:config, {})
|
||||||
beamer.logo_url = attrs[:logo_url]
|
|
||||||
beamer.save!
|
beamer.save!
|
||||||
beamers_by_source_id[source_id] = beamer
|
beamers_by_source_id[source_id] = beamer
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class TournamentSyncSchema
|
||||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
||||||
}.freeze,
|
}.freeze,
|
||||||
Beamer => {
|
Beamer => {
|
||||||
synced: %w[id name display_state is_default qr_code_url logo_url].freeze,
|
synced: %w[id name display_state is_default config].freeze,
|
||||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
||||||
}.freeze,
|
}.freeze,
|
||||||
Stage => {
|
Stage => {
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,5 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
resources :match_scores, only: %i[show update]
|
resources :match_scores, only: %i[show update]
|
||||||
get 'healthz', to: 'health#index'
|
get 'healthz', to: 'health#index'
|
||||||
|
get 'version', to: 'version#show'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ReplaceBeamerAssetFieldsWithConfig < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
add_column :beamers, :config, :json, null: false, default: {}
|
||||||
|
|
||||||
|
execute <<~SQL.squish
|
||||||
|
UPDATE beamers
|
||||||
|
SET display_state = CASE display_state
|
||||||
|
WHEN 'groups' THEN 'group_tables'
|
||||||
|
WHEN 'between_games' THEN 'between_games'
|
||||||
|
WHEN 'during_game' THEN 'during_game'
|
||||||
|
ELSE 'during_game'
|
||||||
|
END
|
||||||
|
SQL
|
||||||
|
|
||||||
|
change_column_default :beamers, :display_state, from: 'matches_all', to: 'during_game'
|
||||||
|
remove_column :beamers, :qr_code_url, :string
|
||||||
|
remove_column :beamers, :logo_url, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_column :beamers, :qr_code_url, :string
|
||||||
|
add_column :beamers, :logo_url, :string
|
||||||
|
change_column_default :beamers, :display_state, from: 'during_game', to: 'matches_all'
|
||||||
|
remove_column :beamers, :config, :json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BackfillDefaultBeamerConfig < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
execute <<~SQL.squish
|
||||||
|
UPDATE beamers
|
||||||
|
SET config = '{"match_filter":"state:in_progress"}'
|
||||||
|
WHERE name = 'default'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute <<~SQL.squish
|
||||||
|
UPDATE beamers
|
||||||
|
SET config = '{}'
|
||||||
|
WHERE name = 'default'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,17 +10,16 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2026_04_24_120000) do
|
ActiveRecord::Schema[7.0].define(version: 2026_04_26_121000) do
|
||||||
create_table "beamers", force: :cascade do |t|
|
create_table "beamers", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "display_state", default: "matches_all", null: false
|
t.string "display_state", default: "during_game", null: false
|
||||||
t.boolean "is_default", default: false
|
t.boolean "is_default", default: false
|
||||||
t.string "qr_code_url"
|
|
||||||
t.string "logo_url"
|
|
||||||
t.integer "tournament_id", null: false
|
t.integer "tournament_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "sync_source_id"
|
t.integer "sync_source_id"
|
||||||
|
t.json "config", default: {}, null: false
|
||||||
t.index ["tournament_id", "name"], name: "index_beamers_on_tournament_id_and_name", unique: true
|
t.index ["tournament_id", "name"], name: "index_beamers_on_tournament_id_and_name", unique: true
|
||||||
t.index ["tournament_id"], name: "index_beamers_on_tournament_id"
|
t.index ["tournament_id"], name: "index_beamers_on_tournament_id"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@ Initial subscribe snapshot:
|
||||||
"beamer": {
|
"beamer": {
|
||||||
"id": 34,
|
"id": 34,
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"display_state": "matches_all",
|
"display_state": "during_game",
|
||||||
"qr_code_url": null,
|
"config": {
|
||||||
"logo_url": null
|
"match_filter": "state:in_progress"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,15 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe_matches!(tournament_id:, state: nil)
|
def subscribe_matches!(tournament_id:, **filters)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentMatchesChannel',
|
channel: 'TournamentMatchesChannel',
|
||||||
tournament_id:,
|
tournament_id:,
|
||||||
state:
|
state: filters[:state],
|
||||||
|
stage_id: filters[:stage_id],
|
||||||
|
stage_level: filters[:stage_level],
|
||||||
|
position: filters[:position],
|
||||||
|
group_stage_position: filters[:group_stage_position]
|
||||||
}.compact.to_json
|
}.compact.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,11 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
|
|
||||||
describe 'POST #create' do
|
describe 'POST #create' do
|
||||||
let(:valid_params) do
|
let(:valid_params) do
|
||||||
{ name: 'new-beamer', display_state: 'groups' }
|
{
|
||||||
|
name: 'new-beamer',
|
||||||
|
display_state: 'during_game',
|
||||||
|
config: { match_filter: 'state:in_progress' }
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'as owner' do
|
context 'as owner' do
|
||||||
|
|
@ -83,7 +87,8 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
post :create, params: { tournament_id: @tournament.to_param }.merge(valid_params)
|
||||||
body = deserialize_response response
|
body = deserialize_response response
|
||||||
expect(body[:name]).to eq('new-beamer')
|
expect(body[:name]).to eq('new-beamer')
|
||||||
expect(body[:display_state]).to eq('groups')
|
expect(body[:display_state]).to eq('during_game')
|
||||||
|
expect(body[:config]).to eq(match_filter: 'state:in_progress')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects invalid display_state' do
|
it 'rejects invalid display_state' do
|
||||||
|
|
@ -95,7 +100,7 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
|
|
||||||
it 'rejects duplicate name in same tournament' do
|
it 'rejects duplicate name in same tournament' do
|
||||||
post :create, params: {
|
post :create, params: {
|
||||||
tournament_id: @tournament.to_param, name: @beamer.name, display_state: 'matches_all'
|
tournament_id: @tournament.to_param, name: @beamer.name, display_state: 'during_game'
|
||||||
}
|
}
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
|
|
@ -133,15 +138,18 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
expect(@beamer.reload.display_state).to eq('during_game')
|
expect(@beamer.reload.display_state).to eq('during_game')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates optional fields' do
|
it 'replaces config' do
|
||||||
|
@beamer.update!(config: { match_filter: 'state:upcoming', image_key: 'old-logo' })
|
||||||
|
|
||||||
patch :update, params: {
|
patch :update, params: {
|
||||||
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
qr_code_url: 'https://example.com/qr', logo_url: '/static/logo.png'
|
config: { image_key: 'bpwstr_logo' }
|
||||||
}
|
}
|
||||||
expect(response).to be_successful
|
expect(response).to be_successful
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body[:config]).to eq(image_key: 'bpwstr_logo')
|
||||||
@beamer.reload
|
@beamer.reload
|
||||||
expect(@beamer.qr_code_url).to eq('https://example.com/qr')
|
expect(@beamer.config).to eq('image_key' => 'bpwstr_logo')
|
||||||
expect(@beamer.logo_url).to eq('/static/logo.png')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects invalid display_state' do
|
it 'rejects invalid display_state' do
|
||||||
|
|
@ -159,7 +167,7 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
it 'returns forbidden' do
|
it 'returns forbidden' do
|
||||||
patch :update, params: {
|
patch :update, params: {
|
||||||
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
display_state: 'groups'
|
display_state: 'group_tables'
|
||||||
}
|
}
|
||||||
expect(response).to have_http_status(:forbidden)
|
expect(response).to have_http_status(:forbidden)
|
||||||
end
|
end
|
||||||
|
|
@ -174,7 +182,7 @@ RSpec.describe BeamersController, type: :controller do
|
||||||
it 'returns locked' do
|
it 'returns locked' do
|
||||||
patch :update, params: {
|
patch :update, params: {
|
||||||
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
tournament_id: @tournament.to_param, id: @beamer.to_param,
|
||||||
display_state: 'groups'
|
display_state: 'group_tables'
|
||||||
}
|
}
|
||||||
expect(response).to have_http_status(:locked)
|
expect(response).to have_http_status(:locked)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,27 @@ RSpec.describe MatchesController, type: :controller do
|
||||||
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
expect(match.keys).to match_array(%i[id position state teams match_scores group stage])
|
||||||
expect(match[:teams]).to all(include(:id, :name))
|
expect(match[:teams]).to all(include(:id, :name))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'filters group stage matches by position across groups' do
|
||||||
|
get :index, params: { tournament_id: @tournament.to_param, group_stage_position: 0 }
|
||||||
|
body = deserialize_response(response)
|
||||||
|
|
||||||
|
expect(body).not_to be_empty
|
||||||
|
expect(body).to all(include(position: 0))
|
||||||
|
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([-1])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on a playoff tournament' do
|
||||||
|
it 'filters matches by stage level' do
|
||||||
|
stage = @tournament.stages.find_by!(level: 1)
|
||||||
|
|
||||||
|
get :index, params: { tournament_id: @tournament.to_param, stage_level: stage.level }
|
||||||
|
body = deserialize_response(response)
|
||||||
|
|
||||||
|
expect(body.map { |match| match.dig(:stage, :level) }.uniq).to eq([stage.level])
|
||||||
|
expect(body.map { |match| match[:id] }).to match_array(stage.matches.map(&:id))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# TODO: add test for upcoming once there is test data for a "valid" group stage
|
# TODO: add test for upcoming once there is test data for a "valid" group stage
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(response.dig(:json, :errors)).to eq([])
|
expect(response.dig(:json, :errors)).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'reports running version' do
|
||||||
|
response = anonymous_client.get('/version')
|
||||||
|
|
||||||
|
expect(response[:status]).to eq(200)
|
||||||
|
expect(response.dig(:json, :git_commit_sha)).to match(/\A[0-9a-f]{40}\z/)
|
||||||
|
end
|
||||||
|
|
||||||
it 'supports registration for new users and still requires confirmation for login' do
|
it 'supports registration for new users and still requires confirmation for login' do
|
||||||
unique = unique_suffix
|
unique = unique_suffix
|
||||||
email = "http-e2e-#{unique}@example.com"
|
email = "http-e2e-#{unique}@example.com"
|
||||||
|
|
@ -206,6 +213,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
params: { state: 'not_started' })
|
params: { state: 'not_started' })
|
||||||
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
matches_upcoming = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
params: { state: 'upcoming' })
|
params: { state: 'upcoming' })
|
||||||
|
matches_group_position = anonymous_client.get(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { group_stage_position: match.fetch(:position) }
|
||||||
|
)
|
||||||
|
matches_group_stage = anonymous_client.get(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
|
params: { stage_level: group_stage.fetch(:level) }
|
||||||
|
)
|
||||||
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
match_show = anonymous_client.get("/matches/#{match.fetch(:id)}")
|
||||||
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
invalid_match_update = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'bogus' })
|
||||||
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
forbidden_match_update = other_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
|
||||||
|
|
@ -276,6 +291,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(matches_not_started[:json].pluck(:id)).to include(match.fetch(:id))
|
expect(matches_not_started[:json].pluck(:id)).to include(match.fetch(:id))
|
||||||
expect(matches_upcoming[:status]).to eq(200)
|
expect(matches_upcoming[:status]).to eq(200)
|
||||||
expect(matches_upcoming[:json].pluck(:id)).to include(match.fetch(:id))
|
expect(matches_upcoming[:json].pluck(:id)).to include(match.fetch(:id))
|
||||||
|
expect(matches_group_position[:status]).to eq(200)
|
||||||
|
expect(matches_group_position[:json].map { |item| item.dig(:stage, :level) }.uniq).to eq([-1])
|
||||||
|
expect(matches_group_position[:json]).to all(include(position: match.fetch(:position)))
|
||||||
|
expect(matches_group_stage[:status]).to eq(200)
|
||||||
|
expect(matches_group_stage[:json].count).to eq(tournament_match_count(tournament))
|
||||||
|
expect(matches_group_stage[:json].map { |item| item.dig(:stage, :id) }.uniq).to eq([group_stage.fetch(:id)])
|
||||||
expect(match_show[:status]).to eq(200)
|
expect(match_show[:status]).to eq(200)
|
||||||
expect(invalid_match_update[:status]).to eq(422)
|
expect(invalid_match_update[:status]).to eq(422)
|
||||||
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
expect(invalid_match_update.dig(:json, :error)).to eq('Invalid target state')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'securerandom'
|
||||||
|
require_relative '../../../e2e/lib/api_client'
|
||||||
|
|
||||||
|
RSpec.describe 'Tournament beamer HTTP E2E' do
|
||||||
|
required_env_vars = %w[
|
||||||
|
TURNIERE_E2E_BASE_URL
|
||||||
|
TURNIERE_E2E_EMAIL
|
||||||
|
TURNIERE_E2E_PASSWORD
|
||||||
|
TURNIERE_E2E_ALT_EMAIL
|
||||||
|
TURNIERE_E2E_ALT_PASSWORD
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
before do
|
||||||
|
missing_vars = required_env_vars.reject { |name| ENV.key?(name) }
|
||||||
|
skip("HTTP E2E requires #{missing_vars.join(', ')}") unless missing_vars.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL') }
|
||||||
|
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
||||||
|
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||||
|
let(:other_email) { ENV.fetch('TURNIERE_E2E_ALT_EMAIL') }
|
||||||
|
let(:other_password) { ENV.fetch('TURNIERE_E2E_ALT_PASSWORD') }
|
||||||
|
let(:owner_client) { login_client(email: owner_email, password: owner_password) }
|
||||||
|
let(:other_client) { login_client(email: other_email, password: other_password) }
|
||||||
|
let(:anonymous_client) { TurniereE2E::ApiClient.new(base_url:) }
|
||||||
|
|
||||||
|
it 'exercises beamer control endpoints over HTTP' do
|
||||||
|
tournament = create_tournament(client: owner_client, name_prefix: 'Beamer Control')
|
||||||
|
|
||||||
|
default_beamer = anonymous_client.get(
|
||||||
|
beamer_by_name_path(tournament.fetch(:id)),
|
||||||
|
params: { name: 'missing' }
|
||||||
|
)
|
||||||
|
default_show = anonymous_client.get(beamer_path(tournament.fetch(:id), default_beamer.dig(:json, :id)))
|
||||||
|
|
||||||
|
expect(default_beamer[:status]).to eq(200)
|
||||||
|
expect(default_beamer.dig(:json, :name)).to eq('default')
|
||||||
|
expect(default_beamer.dig(:json, :display_state)).to eq('during_game')
|
||||||
|
expect(default_beamer.dig(:json, :config)).to eq(match_filter: 'state:in_progress')
|
||||||
|
expect(default_show[:status]).to eq(200)
|
||||||
|
expect(default_show.dig(:json, :config)).to eq(match_filter: 'state:in_progress')
|
||||||
|
|
||||||
|
create_response = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
|
||||||
|
name: 'hall-a',
|
||||||
|
display_state: 'group_tables',
|
||||||
|
config: {
|
||||||
|
match_filter: 'stage:groups',
|
||||||
|
table_view: 'compact'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(create_response[:status]).to eq(201)
|
||||||
|
beamer = create_response.fetch(:json)
|
||||||
|
expect(beamer.fetch(:name)).to eq('hall-a')
|
||||||
|
expect(beamer.fetch(:display_state)).to eq('group_tables')
|
||||||
|
expect(beamer.fetch(:config)).to eq(match_filter: 'stage:groups', table_view: 'compact')
|
||||||
|
expect(beamer).not_to have_key(:qr_code_url)
|
||||||
|
expect(beamer).not_to have_key(:logo_url)
|
||||||
|
|
||||||
|
index_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers")
|
||||||
|
show_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
|
||||||
|
by_name_response = anonymous_client.get(beamer_by_name_path(tournament.fetch(:id)), params: { name: 'hall-a' })
|
||||||
|
|
||||||
|
expect(index_response[:status]).to eq(200)
|
||||||
|
expect(index_response.fetch(:json).map { |item| item.fetch(:name) }).to contain_exactly('default', 'hall-a')
|
||||||
|
expect(show_response[:status]).to eq(200)
|
||||||
|
expect(show_response.dig(:json, :id)).to eq(beamer.fetch(:id))
|
||||||
|
expect(by_name_response[:status]).to eq(200)
|
||||||
|
expect(by_name_response.dig(:json, :id)).to eq(beamer.fetch(:id))
|
||||||
|
|
||||||
|
update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||||
|
display_state: 'static_image',
|
||||||
|
config: {
|
||||||
|
image_key: 'bpwstr_logo'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(update_response[:status]).to eq(200)
|
||||||
|
expect(update_response.dig(:json, :display_state)).to eq('static_image')
|
||||||
|
expect(update_response.dig(:json, :config)).to eq(image_key: 'bpwstr_logo')
|
||||||
|
expect(update_response[:json]).not_to have_key(:qr_code_url)
|
||||||
|
expect(update_response[:json]).not_to have_key(:logo_url)
|
||||||
|
|
||||||
|
invalid_update = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||||
|
display_state: 'bogus'
|
||||||
|
})
|
||||||
|
invalid_config_update = owner_client.patch(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}",
|
||||||
|
body: { config: ['not-an-object'] }
|
||||||
|
)
|
||||||
|
duplicate_create = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
|
||||||
|
name: 'hall-a',
|
||||||
|
display_state: 'during_game'
|
||||||
|
})
|
||||||
|
forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||||
|
display_state: 'group_tables'
|
||||||
|
})
|
||||||
|
unauthenticated_create = anonymous_client.post(
|
||||||
|
"/tournaments/#{tournament.fetch(:id)}/beamers",
|
||||||
|
body: {
|
||||||
|
name: 'anon',
|
||||||
|
display_state: 'group_tables'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(invalid_update[:status]).to eq(422)
|
||||||
|
expect(invalid_config_update[:status]).to eq(422)
|
||||||
|
expect(duplicate_create[:status]).to eq(422)
|
||||||
|
expect(forbidden_update[:status]).to eq(403)
|
||||||
|
expect(unauthenticated_create[:status]).to eq(401)
|
||||||
|
|
||||||
|
read_only_tournament = create_tournament(
|
||||||
|
client: owner_client,
|
||||||
|
name_prefix: 'Beamer Read Only',
|
||||||
|
read_only_mode: true
|
||||||
|
)
|
||||||
|
read_only_beamer = anonymous_client.get(
|
||||||
|
beamer_by_name_path(read_only_tournament.fetch(:id)),
|
||||||
|
params: { name: 'default' }
|
||||||
|
)
|
||||||
|
locked_update = owner_client.patch(
|
||||||
|
beamer_path(read_only_tournament.fetch(:id), read_only_beamer.dig(:json, :id)),
|
||||||
|
body: { display_state: 'group_tables' }
|
||||||
|
)
|
||||||
|
expect(locked_update[:status]).to eq(423)
|
||||||
|
|
||||||
|
delete_response = owner_client.delete("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
|
||||||
|
deleted_show = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
|
||||||
|
deleted_by_name = anonymous_client.get(beamer_by_name_path(tournament.fetch(:id)), params: { name: 'hall-a' })
|
||||||
|
|
||||||
|
expect(delete_response[:status]).to eq(204)
|
||||||
|
expect(deleted_show[:status]).to eq(404)
|
||||||
|
expect(deleted_by_name[:status]).to eq(200)
|
||||||
|
expect(deleted_by_name.dig(:json, :name)).to eq('default')
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_client(email:, password:)
|
||||||
|
api_client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
|
response = api_client.login!(email:, password:)
|
||||||
|
|
||||||
|
expect(response[:status]).to eq(200)
|
||||||
|
api_client
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_tournament(client:, name_prefix:, read_only_mode: false)
|
||||||
|
response = client.post('/tournaments', body: {
|
||||||
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
|
description: 'HTTP beamer control tournament',
|
||||||
|
public: true,
|
||||||
|
read_only_mode:,
|
||||||
|
teams: Array.new(4) do |index|
|
||||||
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
|
end
|
||||||
|
})
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def beamer_path(tournament_id, beamer_id)
|
||||||
|
"/tournaments/#{tournament_id}/beamers/#{beamer_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def beamer_by_name_path(tournament_id)
|
||||||
|
"/tournaments/#{tournament_id}/beamers/by_name"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -26,7 +26,10 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
||||||
tournament = create_tournament(name_prefix: 'Ws Beamer')
|
tournament = create_tournament(name_prefix: 'Ws Beamer')
|
||||||
create_beamer = client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
|
create_beamer = client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
|
||||||
name: 'projector-a',
|
name: 'projector-a',
|
||||||
display_state: 'matches_all'
|
display_state: 'during_game',
|
||||||
|
config: {
|
||||||
|
match_filter: 'state:in_progress'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
expect(create_beamer[:status]).to eq(201)
|
expect(create_beamer[:status]).to eq(201)
|
||||||
beamer = create_beamer.fetch(:json)
|
beamer = create_beamer.fetch(:json)
|
||||||
|
|
@ -37,22 +40,25 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
||||||
initial_payload = cable_client.wait_for_beamer_payload!
|
initial_payload = cable_client.wait_for_beamer_payload!
|
||||||
expect(initial_payload.fetch('type')).to eq('beamer.snapshot')
|
expect(initial_payload.fetch('type')).to eq('beamer.snapshot')
|
||||||
expect(initial_payload.dig('beamer', 'id')).to eq(beamer.fetch(:id))
|
expect(initial_payload.dig('beamer', 'id')).to eq(beamer.fetch(:id))
|
||||||
expect(initial_payload.dig('beamer', 'display_state')).to eq('matches_all')
|
expect(initial_payload.dig('beamer', 'display_state')).to eq('during_game')
|
||||||
|
expect(initial_payload.dig('beamer', 'config', 'match_filter')).to eq('state:in_progress')
|
||||||
|
|
||||||
update = client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
update = client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||||
display_state: 'during_game',
|
display_state: 'static_image',
|
||||||
qr_code_url: 'https://example.com/qr',
|
config: {
|
||||||
logo_url: '/static/logo.png'
|
image_key: 'bpwstr_logo'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
expect(update[:status]).to eq(200)
|
expect(update[:status]).to eq(200)
|
||||||
|
|
||||||
updated_payload = wait_for_beamer_payload!(
|
updated_payload = wait_for_beamer_payload!(
|
||||||
cable_client,
|
cable_client,
|
||||||
event_type: 'beamer.updated',
|
event_type: 'beamer.updated',
|
||||||
display_state: 'during_game'
|
display_state: 'static_image'
|
||||||
)
|
)
|
||||||
expect(updated_payload.dig('beamer', 'qr_code_url')).to eq('https://example.com/qr')
|
expect(updated_payload.dig('beamer', 'config')).to eq('image_key' => 'bpwstr_logo')
|
||||||
expect(updated_payload.dig('beamer', 'logo_url')).to eq('/static/logo.png')
|
expect(updated_payload['beamer']).not_to have_key('qr_code_url')
|
||||||
|
expect(updated_payload['beamer']).not_to have_key('logo_url')
|
||||||
|
|
||||||
delete = client.delete("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
|
delete = client.delete("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}")
|
||||||
expect(delete[:status]).to eq(204)
|
expect(delete[:status]).to eq(204)
|
||||||
|
|
@ -60,7 +66,7 @@ RSpec.describe 'Tournament beamer websocket HTTP E2E' do
|
||||||
deleted_payload = wait_for_beamer_payload!(
|
deleted_payload = wait_for_beamer_payload!(
|
||||||
cable_client,
|
cable_client,
|
||||||
event_type: 'beamer.deleted',
|
event_type: 'beamer.deleted',
|
||||||
display_state: 'during_game'
|
display_state: 'static_image'
|
||||||
)
|
)
|
||||||
expect(deleted_payload.dig('beamer', 'name')).to eq('projector-a')
|
expect(deleted_payload.dig('beamer', 'name')).to eq('projector-a')
|
||||||
ensure
|
ensure
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'securerandom'
|
||||||
|
require_relative '../../../e2e/lib/api_client'
|
||||||
|
|
||||||
|
RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
||||||
|
required_env_vars = %w[
|
||||||
|
TURNIERE_E2E_EMAIL
|
||||||
|
TURNIERE_E2E_PASSWORD
|
||||||
|
TURNIERE_SOURCE_E2E_BASE_URL
|
||||||
|
TURNIERE_FOLLOWER_E2E_BASE_URL
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
before do
|
||||||
|
missing_vars = required_env_vars.reject { |name| ENV.key?(name) }
|
||||||
|
skip("HTTP E2E requires #{missing_vars.join(', ')}") unless missing_vars.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:source_base_url) { ENV.fetch('TURNIERE_SOURCE_E2E_BASE_URL') }
|
||||||
|
let(:follower_base_url) { ENV.fetch('TURNIERE_FOLLOWER_E2E_BASE_URL') }
|
||||||
|
let(:owner_email) { ENV.fetch('TURNIERE_E2E_EMAIL') }
|
||||||
|
let(:owner_password) { ENV.fetch('TURNIERE_E2E_PASSWORD') }
|
||||||
|
let(:sync_token) { "sync-#{SecureRandom.hex(8)}" }
|
||||||
|
let(:source_client) { login_client(base_url: source_base_url) }
|
||||||
|
let(:follower_client) { login_client(base_url: follower_base_url) }
|
||||||
|
let(:follower_read_client) { TurniereE2E::ApiClient.new(base_url: follower_base_url) }
|
||||||
|
|
||||||
|
it 'mirrors leader beamer control changes into follower HTTP reads' do
|
||||||
|
source = create_tournament(client: source_client, name_prefix: 'Beamer Source')
|
||||||
|
follower = create_tournament(client: follower_client, name_prefix: 'Beamer Follower', read_only_mode: true)
|
||||||
|
|
||||||
|
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||||
|
sync_target_url: sync_state_url(follower.fetch(:id)),
|
||||||
|
sync_auth_token: sync_token
|
||||||
|
})
|
||||||
|
expect(configure_sync[:status]).to eq(200)
|
||||||
|
sync_now_if_requested!(source.fetch(:id))
|
||||||
|
|
||||||
|
source_default = fetch_beamer(client: source_client, tournament_id: source.fetch(:id), beamer_name: 'default')
|
||||||
|
follower_default = wait_for_follower_beamer!(
|
||||||
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: 'default',
|
||||||
|
display_state: source_default.fetch(:display_state),
|
||||||
|
config: source_default.fetch(:config)
|
||||||
|
)
|
||||||
|
expect(follower_default.fetch(:name)).to eq('default')
|
||||||
|
|
||||||
|
leader_default_update = source_client.patch(
|
||||||
|
beamer_path(source.fetch(:id), source_default.fetch(:id)),
|
||||||
|
body: {
|
||||||
|
display_state: 'between_games',
|
||||||
|
config: {
|
||||||
|
match_filter: 'state:queued',
|
||||||
|
rotation_seconds: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(leader_default_update[:status]).to eq(200)
|
||||||
|
sync_now_if_requested!(source.fetch(:id))
|
||||||
|
|
||||||
|
mirrored_default = wait_for_follower_beamer!(
|
||||||
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: 'default',
|
||||||
|
display_state: 'between_games',
|
||||||
|
config: {
|
||||||
|
match_filter: 'state:queued',
|
||||||
|
rotation_seconds: 10
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(mirrored_default.fetch(:name)).to eq('default')
|
||||||
|
expect(mirrored_default).not_to have_key(:qr_code_url)
|
||||||
|
expect(mirrored_default).not_to have_key(:logo_url)
|
||||||
|
|
||||||
|
leader_create = source_client.post("/tournaments/#{source.fetch(:id)}/beamers", body: {
|
||||||
|
name: 'stage-left',
|
||||||
|
display_state: 'group_tables',
|
||||||
|
config: {
|
||||||
|
match_filter: 'stage:groups',
|
||||||
|
table_view: 'compact'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(leader_create[:status]).to eq(201)
|
||||||
|
sync_now_if_requested!(source.fetch(:id))
|
||||||
|
|
||||||
|
mirrored_created = wait_for_follower_beamer!(
|
||||||
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: 'stage-left',
|
||||||
|
display_state: 'group_tables',
|
||||||
|
config: {
|
||||||
|
match_filter: 'stage:groups',
|
||||||
|
table_view: 'compact'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(mirrored_created.fetch(:name)).to eq('stage-left')
|
||||||
|
expect(mirrored_created).not_to have_key(:qr_code_url)
|
||||||
|
expect(mirrored_created).not_to have_key(:logo_url)
|
||||||
|
|
||||||
|
leader_update = source_client.patch(
|
||||||
|
beamer_path(source.fetch(:id), leader_create.dig(:json, :id)),
|
||||||
|
body: {
|
||||||
|
display_state: 'static_image',
|
||||||
|
config: {
|
||||||
|
image_key: 'stage-left-live'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(leader_update[:status]).to eq(200)
|
||||||
|
sync_now_if_requested!(source.fetch(:id))
|
||||||
|
|
||||||
|
mirrored_updated = wait_for_follower_beamer!(
|
||||||
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: 'stage-left',
|
||||||
|
display_state: 'static_image',
|
||||||
|
config: {
|
||||||
|
image_key: 'stage-left-live'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(mirrored_updated).not_to have_key(:qr_code_url)
|
||||||
|
expect(mirrored_updated).not_to have_key(:logo_url)
|
||||||
|
|
||||||
|
locked_follower_update = follower_client.patch(
|
||||||
|
beamer_path(follower.fetch(:id), mirrored_updated.fetch(:id)),
|
||||||
|
body: { display_state: 'during_game' }
|
||||||
|
)
|
||||||
|
expect(locked_follower_update[:status]).to eq(423)
|
||||||
|
|
||||||
|
leader_delete = source_client.delete("/tournaments/#{source.fetch(:id)}/beamers/#{leader_create.dig(:json, :id)}")
|
||||||
|
expect(leader_delete[:status]).to eq(204)
|
||||||
|
sync_now_if_requested!(source.fetch(:id))
|
||||||
|
|
||||||
|
wait_for_follower_beamer_removed!(
|
||||||
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: 'stage-left'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_client(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)
|
||||||
|
api_client
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_tournament(client:, name_prefix:, read_only_mode: false)
|
||||||
|
response = client.post('/tournaments', body: tournament_payload(name_prefix:, read_only_mode:))
|
||||||
|
expect(response[:status]).to eq(201)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_beamer(client:, tournament_id:, beamer_name:)
|
||||||
|
response = client.get(beamer_by_name_path(tournament_id), params: { name: beamer_name })
|
||||||
|
expect(response[:status]).to eq(200)
|
||||||
|
response.fetch(:json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_now_if_requested!(source_tournament_id)
|
||||||
|
return unless ENV.fetch('TURNIERE_E2E_SYNC_MODE', nil) == 'manual'
|
||||||
|
|
||||||
|
response = source_client.post("/tournaments/#{source_tournament_id}/test_sync")
|
||||||
|
expect(response[:status]).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_follower_beamer!(tournament_id:, beamer_name:, timeout: 20, **expected_attrs)
|
||||||
|
deadline = Time.now.utc + timeout
|
||||||
|
loop do
|
||||||
|
response = follower_read_client.get(beamer_by_name_path(tournament_id), params: { name: beamer_name })
|
||||||
|
if response[:status] == 200 && response.dig(:json, :name) == beamer_name
|
||||||
|
beamer = response.fetch(:json)
|
||||||
|
return beamer if expected_attrs.all? { |key, value| beamer[key] == value }
|
||||||
|
end
|
||||||
|
|
||||||
|
raise "follower beamer #{beamer_name} not synced within #{timeout}s" if Time.now.utc >= deadline
|
||||||
|
|
||||||
|
sleep 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_follower_beamer_removed!(tournament_id:, beamer_name:, timeout: 20)
|
||||||
|
deadline = Time.now.utc + timeout
|
||||||
|
loop do
|
||||||
|
response = follower_read_client.get(beamer_by_name_path(tournament_id), params: { name: beamer_name })
|
||||||
|
return if response[:status] == 200 && response.dig(:json, :name) != beamer_name
|
||||||
|
|
||||||
|
raise "follower beamer #{beamer_name} not removed within #{timeout}s" if Time.now.utc >= deadline
|
||||||
|
|
||||||
|
sleep 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tournament_payload(name_prefix:, read_only_mode:)
|
||||||
|
{
|
||||||
|
name: "#{name_prefix} #{SecureRandom.hex(3)}",
|
||||||
|
description: 'Follower beamer HTTP tournament',
|
||||||
|
public: true,
|
||||||
|
read_only_mode:,
|
||||||
|
sync_auth_token: read_only_mode ? sync_token : nil,
|
||||||
|
teams: Array.new(4) do |index|
|
||||||
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
|
end
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_state_url(tournament_id)
|
||||||
|
"#{follower_base_url}/tournaments/#{tournament_id}/sync_state"
|
||||||
|
end
|
||||||
|
|
||||||
|
def beamer_path(tournament_id, beamer_id)
|
||||||
|
"/tournaments/#{tournament_id}/beamers/#{beamer_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def beamer_by_name_path(tournament_id)
|
||||||
|
"/tournaments/#{tournament_id}/beamers/by_name"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -32,7 +32,7 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
follower = create_follower_tournament(name_prefix: 'Ws Beamer Follower')
|
follower = create_follower_tournament(name_prefix: 'Ws Beamer Follower')
|
||||||
|
|
||||||
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
configure_sync = source_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||||
sync_target_url: "#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state",
|
sync_target_url: follower_sync_url(follower),
|
||||||
sync_auth_token: sync_token
|
sync_auth_token: sync_token
|
||||||
})
|
})
|
||||||
expect(configure_sync[:status]).to eq(200)
|
expect(configure_sync[:status]).to eq(200)
|
||||||
|
|
@ -40,23 +40,35 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
|
follower_default_beamer = wait_for_default_beamer!(tournament_id: follower.fetch(:id))
|
||||||
|
|
||||||
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
cable_client = TurniereE2E::ActionCableClient.new(base_url: follower_base_url)
|
||||||
cable_client.subscribe_beamer!(tournament_id: follower.fetch(:id),
|
cable_client.subscribe_beamer!(
|
||||||
beamer_name: follower_default_beamer.fetch(:name))
|
tournament_id: follower.fetch(:id),
|
||||||
|
beamer_name: follower_default_beamer.fetch(:name)
|
||||||
|
)
|
||||||
cable_client.wait_for_beamer_payload!
|
cable_client.wait_for_beamer_payload!
|
||||||
|
|
||||||
source_default_beamer = fetch_beamer(base_url: source_base_url, tournament_id: source.fetch(:id),
|
source_default_beamer = fetch_beamer(
|
||||||
beamer_name: DEFAULT_BEAMER_NAME)
|
base_url: source_base_url,
|
||||||
update = source_client.patch("/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}", body: {
|
tournament_id: source.fetch(:id),
|
||||||
display_state: 'groups'
|
beamer_name: DEFAULT_BEAMER_NAME
|
||||||
})
|
)
|
||||||
|
update = source_client.patch(
|
||||||
|
"/tournaments/#{source.fetch(:id)}/beamers/#{source_default_beamer.fetch(:id)}",
|
||||||
|
body: {
|
||||||
|
display_state: 'static_image',
|
||||||
|
config: {
|
||||||
|
image_key: 'bpwstr_logo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
expect(update[:status]).to eq(200)
|
expect(update[:status]).to eq(200)
|
||||||
|
|
||||||
follower_payload = wait_for_beamer_payload!(
|
follower_payload = wait_for_beamer_payload!(
|
||||||
cable_client,
|
cable_client,
|
||||||
event_type: 'beamer.updated',
|
event_type: 'beamer.updated',
|
||||||
display_state: 'groups'
|
display_state: 'static_image'
|
||||||
)
|
)
|
||||||
expect(follower_payload.dig('beamer', 'name')).to eq(DEFAULT_BEAMER_NAME)
|
expect(follower_payload.dig('beamer', 'name')).to eq(DEFAULT_BEAMER_NAME)
|
||||||
|
expect(follower_payload.dig('beamer', 'config')).to eq('image_key' => 'bpwstr_logo')
|
||||||
ensure
|
ensure
|
||||||
cable_client&.close
|
cable_client&.close
|
||||||
end
|
end
|
||||||
|
|
@ -101,6 +113,10 @@ RSpec.describe 'Tournament follower sync beamer websocket HTTP E2E' do
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def follower_sync_url(follower)
|
||||||
|
"#{follower_base_url}/tournaments/#{follower.fetch(:id)}/sync_state"
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_beamer(base_url:, tournament_id:, beamer_name:)
|
def fetch_beamer(base_url:, tournament_id:, beamer_name:)
|
||||||
client = TurniereE2E::ApiClient.new(base_url:)
|
client = TurniereE2E::ApiClient.new(base_url:)
|
||||||
response = client.get("/tournaments/#{tournament_id}/beamers/by_name", params: { name: beamer_name })
|
response = client.get("/tournaments/#{tournament_id}/beamers/by_name", params: { name: beamer_name })
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,20 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
|
|
||||||
tournament_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
tournament_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
filtered_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
stage_matches_cable = TurniereE2E::ActionCableClient.new(base_url:)
|
||||||
|
group_position_filter = { group_stage_position: match.fetch(:position) }
|
||||||
|
stage_filter = { stage_level: group_stage.fetch(:level) }
|
||||||
tournament_cable.subscribe_tournament!(tournament_id: tournament.fetch(:id))
|
tournament_cable.subscribe_tournament!(tournament_id: tournament.fetch(:id))
|
||||||
matches_cable.subscribe_matches!(tournament_id: tournament.fetch(:id))
|
matches_cable.subscribe_matches!(tournament_id: tournament.fetch(:id))
|
||||||
|
filtered_matches_cable.subscribe_matches!(
|
||||||
|
tournament_id: tournament.fetch(:id),
|
||||||
|
**group_position_filter
|
||||||
|
)
|
||||||
|
stage_matches_cable.subscribe_matches!(
|
||||||
|
tournament_id: tournament.fetch(:id),
|
||||||
|
**stage_filter
|
||||||
|
)
|
||||||
|
|
||||||
expect(tournament_cable.wait_for_tournament_payload!.fetch('tournament')).to(
|
expect(tournament_cable.wait_for_tournament_payload!.fetch('tournament')).to(
|
||||||
eq(json_for_api(fetch_tournament(tournament.fetch(:id))))
|
eq(json_for_api(fetch_tournament(tournament.fetch(:id))))
|
||||||
|
|
@ -38,6 +50,12 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
expect(matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
expect(matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||||
)
|
)
|
||||||
|
expect(filtered_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||||
|
)
|
||||||
|
expect(stage_matches_cable.wait_for_matches_payload!.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||||
|
)
|
||||||
|
|
||||||
play_match!(match.fetch(:id), first_score: 3, second_score: 1)
|
play_match!(match.fetch(:id), first_score: 3, second_score: 1)
|
||||||
|
|
||||||
|
|
@ -55,9 +73,29 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
|
|
||||||
matches_payload = wait_for_matches_payload!(matches_cable, match_id: match.fetch(:id), state: 'finished')
|
matches_payload = wait_for_matches_payload!(matches_cable, match_id: match.fetch(:id), state: 'finished')
|
||||||
expect(matches_payload.fetch('matches')).to eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
expect(matches_payload.fetch('matches')).to eq(json_for_api(fetch_matches(tournament.fetch(:id))))
|
||||||
|
|
||||||
|
filtered_matches_payload = wait_for_matches_payload!(
|
||||||
|
filtered_matches_cable,
|
||||||
|
match_id: match.fetch(:id),
|
||||||
|
state: 'finished'
|
||||||
|
)
|
||||||
|
expect(filtered_matches_payload.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: group_position_filter)))
|
||||||
|
)
|
||||||
|
|
||||||
|
stage_matches_payload = wait_for_matches_payload!(
|
||||||
|
stage_matches_cable,
|
||||||
|
match_id: match.fetch(:id),
|
||||||
|
state: 'finished'
|
||||||
|
)
|
||||||
|
expect(stage_matches_payload.fetch('matches')).to(
|
||||||
|
eq(json_for_api(fetch_matches(tournament.fetch(:id), params: stage_filter)))
|
||||||
|
)
|
||||||
ensure
|
ensure
|
||||||
tournament_cable&.close
|
tournament_cable&.close
|
||||||
matches_cable&.close
|
matches_cable&.close
|
||||||
|
filtered_matches_cable&.close
|
||||||
|
stage_matches_cable&.close
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_client
|
def login_client
|
||||||
|
|
@ -111,8 +149,8 @@ RSpec.describe 'Tournament live websocket HTTP E2E' do
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_matches(tournament_id)
|
def fetch_matches(tournament_id, params: {})
|
||||||
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches")
|
response = TurniereE2E::ApiClient.new(base_url:).get("/tournaments/#{tournament_id}/matches", params:)
|
||||||
expect(response[:status]).to eq(200)
|
expect(response[:status]).to eq(200)
|
||||||
response.fetch(:json)
|
response.fetch(:json)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :beamer do
|
factory :beamer do
|
||||||
sequence(:name) { |n| "beamer-#{n}" }
|
sequence(:name) { |n| "beamer-#{n}" }
|
||||||
display_state { 'matches_all' }
|
display_state { 'during_game' }
|
||||||
|
config { {} }
|
||||||
tournament
|
tournament
|
||||||
|
|
||||||
factory :default_beamer do
|
factory :default_beamer do
|
||||||
name { Beamer::DEFAULT_NAME }
|
name { Beamer::DEFAULT_NAME }
|
||||||
|
config { Beamer::DEFAULT_CONFIG }
|
||||||
|
|
||||||
initialize_with do
|
initialize_with do
|
||||||
tournament.beamers.find_or_initialize_by(name:)
|
tournament.beamers.find_or_initialize_by(name:)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ RSpec.describe Beamer, type: :model do
|
||||||
expect(beamer).not_to be_valid
|
expect(beamer).not_to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'requires config to be an object' do
|
||||||
|
beamer = build(:beamer, config: ['not-an-object'])
|
||||||
|
expect(beamer).not_to be_valid
|
||||||
|
expect(beamer.errors[:config]).to include('must be an object')
|
||||||
|
end
|
||||||
|
|
||||||
Beamer::DISPLAY_STATES.each do |state|
|
Beamer::DISPLAY_STATES.each do |state|
|
||||||
it "accepts display_state '#{state}'" do
|
it "accepts display_state '#{state}'" do
|
||||||
beamer = build(:beamer, display_state: state)
|
beamer = build(:beamer, display_state: state)
|
||||||
|
|
@ -54,10 +60,15 @@ RSpec.describe Beamer, type: :model do
|
||||||
expect(args.fetch(:beamer_names)).to eq(['projector'])
|
expect(args.fetch(:beamer_names)).to eq(['projector'])
|
||||||
end
|
end
|
||||||
|
|
||||||
tournament.beamers.create!(name: 'projector', display_state: 'groups')
|
tournament.beamers.create!(
|
||||||
|
name: 'projector',
|
||||||
|
display_state: 'group_tables',
|
||||||
|
config: { 'match_filter' => 'state:in_progress' }
|
||||||
|
)
|
||||||
|
|
||||||
expect(payload[:type]).to eq('beamer.created')
|
expect(payload[:type]).to eq('beamer.created')
|
||||||
expect(payload.dig(:beamer, :display_state)).to eq('groups')
|
expect(payload.dig(:beamer, :display_state)).to eq('group_tables')
|
||||||
|
expect(payload.dig(:beamer, :config)).to eq('match_filter' => 'state:in_progress')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'broadcasts update payload to previous and current beamer names when renamed' do
|
it 'broadcasts update payload to previous and current beamer names when renamed' do
|
||||||
|
|
@ -69,15 +80,17 @@ RSpec.describe Beamer, type: :model do
|
||||||
expect(args.fetch(:beamer_names)).to contain_exactly('old-name', 'new-name')
|
expect(args.fetch(:beamer_names)).to contain_exactly('old-name', 'new-name')
|
||||||
end
|
end
|
||||||
|
|
||||||
beamer.update!(name: 'new-name', display_state: 'during_game')
|
beamer.update!(name: 'new-name', display_state: 'during_game', config: { 'image_key' => 'bpwstr_logo' })
|
||||||
|
|
||||||
expect(payload[:type]).to eq('beamer.updated')
|
expect(payload[:type]).to eq('beamer.updated')
|
||||||
expect(payload.dig(:beamer, :name)).to eq('new-name')
|
expect(payload.dig(:beamer, :name)).to eq('new-name')
|
||||||
expect(payload.dig(:beamer, :display_state)).to eq('during_game')
|
expect(payload.dig(:beamer, :display_state)).to eq('during_game')
|
||||||
|
expect(payload.dig(:beamer, :config)).to eq('image_key' => 'bpwstr_logo')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'broadcasts delete payload with last persisted snapshot' do
|
it 'broadcasts delete payload with last persisted snapshot' do
|
||||||
beamer = create(:beamer, name: 'projector', display_state: 'between_games')
|
beamer = create(:beamer, name: 'projector', display_state: 'between_games',
|
||||||
|
config: { 'match_filter' => 'state:in_progress' })
|
||||||
payload = nil
|
payload = nil
|
||||||
|
|
||||||
expect(BeamerBroadcast).to receive(:call) do |args|
|
expect(BeamerBroadcast).to receive(:call) do |args|
|
||||||
|
|
@ -90,6 +103,7 @@ RSpec.describe Beamer, type: :model do
|
||||||
expect(payload[:type]).to eq('beamer.deleted')
|
expect(payload[:type]).to eq('beamer.deleted')
|
||||||
expect(payload.dig(:beamer, :name)).to eq('projector')
|
expect(payload.dig(:beamer, :name)).to eq('projector')
|
||||||
expect(payload.dig(:beamer, :display_state)).to eq('between_games')
|
expect(payload.dig(:beamer, :display_state)).to eq('between_games')
|
||||||
|
expect(payload.dig(:beamer, :config)).to eq('match_filter' => 'state:in_progress')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,11 @@ RSpec.describe Tournament, type: :model do
|
||||||
describe 'default beamer' do
|
describe 'default beamer' do
|
||||||
it 'creates a default beamer automatically' do
|
it 'creates a default beamer automatically' do
|
||||||
tournament = create(:tournament)
|
tournament = create(:tournament)
|
||||||
|
default_beamer = tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME)
|
||||||
|
|
||||||
expect(tournament.beamers.pluck(:name)).to include(Beamer::DEFAULT_NAME)
|
expect(tournament.beamers.pluck(:name)).to include(Beamer::DEFAULT_NAME)
|
||||||
|
expect(default_beamer.display_state).to eq('during_game')
|
||||||
|
expect(default_beamer.config).to eq(Beamer::DEFAULT_CONFIG)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,29 @@ RSpec.describe 'Tournament live payloads' do
|
||||||
expect(serialized_match[:teams].pluck(:id)).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:teams].pluck(:id)).to match_array(match.teams.map(&:id))
|
||||||
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
expect(serialized_match[:match_scores].map { |score| score[:team][:id] }).to match_array(match.teams.map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'filters group stage matches by position across groups' do
|
||||||
|
tournament = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
|
||||||
|
tournament.group_stage.groups.each do |group|
|
||||||
|
group.matches.each_with_index { |match, index| match.update!(position: index) }
|
||||||
|
end
|
||||||
|
|
||||||
|
matches = described_class.matches_for(tournament, group_stage_position: 0)
|
||||||
|
|
||||||
|
expect(matches).to all(be_group_match)
|
||||||
|
expect(matches.map(&:position).uniq).to eq([0])
|
||||||
|
expect(matches.map { |match| match.group.stage.level }.uniq).to eq([-1])
|
||||||
|
expect(matches.map(&:group_id)).to match_array(tournament.group_stage.groups.map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters all matches belonging to a stage level' do
|
||||||
|
tournament = create(:stage_tournament, stage_count: 2)
|
||||||
|
stage = tournament.stages.find_by!(level: 1)
|
||||||
|
|
||||||
|
matches = described_class.matches_for(tournament, stage_level: stage.level)
|
||||||
|
|
||||||
|
expect(matches).to match_array(stage.matches)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe TournamentBroadcastPayload do
|
describe TournamentBroadcastPayload do
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
describe '.import!' do
|
describe '.import!' do
|
||||||
it 'replaces follower tournament graph using source ids as stable mapping keys' do
|
it 'replaces follower tournament graph using source ids as stable mapping keys' do
|
||||||
source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
|
source = create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match)
|
||||||
source.beamers.create!(name: 'stream-a', display_state: 'during_game', qr_code_url: 'https://example.com/qr')
|
source.beamers.create!(
|
||||||
|
name: 'stream-a',
|
||||||
|
display_state: 'during_game',
|
||||||
|
config: { match_filter: 'state:in_progress' }
|
||||||
|
)
|
||||||
list = create(:team_action_list, tournament: source)
|
list = create(:team_action_list, tournament: source)
|
||||||
create(:team_action_item, team_action_list: list, team: source.teams.first, completed: true)
|
create(:team_action_item, team_action_list: list, team: source.teams.first, completed: true)
|
||||||
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
|
follower = create(:tournament, read_only_mode: true, sync_auth_token: 'shared-secret')
|
||||||
|
|
@ -82,21 +86,18 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
intermediate_round_participants_amount: 0
|
intermediate_round_participants_amount: 0
|
||||||
)
|
)
|
||||||
tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME).update!(
|
tournament.beamers.find_by!(name: Beamer::DEFAULT_NAME).update!(
|
||||||
display_state: 'groups',
|
display_state: 'group_tables',
|
||||||
qr_code_url: 'https://example.com/default-qr',
|
config: { match_filter: 'state:in_progress' }
|
||||||
logo_url: '/logos/default.png'
|
|
||||||
)
|
)
|
||||||
tournament.beamers.create!(
|
tournament.beamers.create!(
|
||||||
name: 'stream-a',
|
name: 'stream-a',
|
||||||
display_state: 'during_game',
|
display_state: 'during_game',
|
||||||
qr_code_url: 'https://example.com/stream-a',
|
config: { match_filter: 'state:in_progress' }
|
||||||
logo_url: '/logos/stream-a.png'
|
|
||||||
)
|
)
|
||||||
tournament.beamers.create!(
|
tournament.beamers.create!(
|
||||||
name: 'stream-b',
|
name: 'stream-b',
|
||||||
display_state: 'between_games',
|
display_state: 'between_games',
|
||||||
qr_code_url: 'https://example.com/stream-b',
|
config: { image_key: 'bpwstr_logo' }
|
||||||
logo_url: '/logos/stream-b.png'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
group_stage = Stage.create!(tournament:, level: -1, state: :finished)
|
group_stage = Stage.create!(tournament:, level: -1, state: :finished)
|
||||||
|
|
@ -204,7 +205,7 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
beamers: tournament.beamers.order(:name).map do |beamer|
|
beamers: tournament.beamers.order(:name).map do |beamer|
|
||||||
beamer.slice(:name, :display_state, :is_default, :qr_code_url, :logo_url).symbolize_keys
|
beamer.slice(:name, :display_state, :is_default, :config).symbolize_keys
|
||||||
end,
|
end,
|
||||||
stages: tournament.stages.order(level: :asc).map do |stage|
|
stages: tournament.stages.order(level: :asc).map do |stage|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
24
tasks.py
24
tasks.py
|
|
@ -235,6 +235,13 @@ def _format_command(command):
|
||||||
return " ".join(shlex.quote(part) for part in command)
|
return " ".join(shlex.quote(part) for part in command)
|
||||||
|
|
||||||
|
|
||||||
|
def _git_commit_sha():
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
def _docker_build_command(tag, target, platforms=None, push=False):
|
def _docker_build_command(tag, target, platforms=None, push=False):
|
||||||
if platforms and "," in str(platforms) and not push:
|
if platforms and "," in str(platforms) and not push:
|
||||||
raise ValueError("multi-platform buildx builds require push=True")
|
raise ValueError("multi-platform buildx builds require push=True")
|
||||||
|
|
@ -246,6 +253,8 @@ def _docker_build_command(tag, target, platforms=None, push=False):
|
||||||
"build",
|
"build",
|
||||||
"--target",
|
"--target",
|
||||||
target,
|
target,
|
||||||
|
"--build-arg",
|
||||||
|
f"GIT_COMMIT_SHA={_git_commit_sha()}",
|
||||||
"-t",
|
"-t",
|
||||||
tag,
|
tag,
|
||||||
"-f",
|
"-f",
|
||||||
|
|
@ -257,7 +266,19 @@ def _docker_build_command(tag, target, platforms=None, push=False):
|
||||||
command.append(".")
|
command.append(".")
|
||||||
return _format_command(command)
|
return _format_command(command)
|
||||||
|
|
||||||
return f"docker build --target {target} -t {tag} -f {DOCKERFILE_PATH} ."
|
return _format_command([
|
||||||
|
"docker",
|
||||||
|
"build",
|
||||||
|
"--target",
|
||||||
|
target,
|
||||||
|
"--build-arg",
|
||||||
|
f"GIT_COMMIT_SHA={_git_commit_sha()}",
|
||||||
|
"-t",
|
||||||
|
tag,
|
||||||
|
"-f",
|
||||||
|
DOCKERFILE_PATH,
|
||||||
|
".",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def _rspec_command(*paths):
|
def _rspec_command(*paths):
|
||||||
|
|
@ -1764,6 +1785,7 @@ def blackbox_follow_service(
|
||||||
alt_username,
|
alt_username,
|
||||||
spec_path=[
|
spec_path=[
|
||||||
"spec/e2e/http/tournament_follow_sync_spec.rb",
|
"spec/e2e/http/tournament_follow_sync_spec.rb",
|
||||||
|
"spec/e2e/http/tournament_follow_sync_beamers_spec.rb",
|
||||||
"spec/e2e/http/tournament_follow_sync_websocket_spec.rb",
|
"spec/e2e/http/tournament_follow_sync_websocket_spec.rb",
|
||||||
"spec/e2e/http/tournament_follow_sync_live_websocket_spec.rb",
|
"spec/e2e/http/tournament_follow_sync_live_websocket_spec.rb",
|
||||||
"spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb",
|
"spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue