Merge branch 'codex/tur-78-beamer-fixes' into 'master'
TUR-78: Beamer fixes See merge request turniere/turniere-backend!63
This commit is contained in:
commit
10f1a56734
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Beamer < ApplicationRecord
|
||||
DEFAULT_NAME = 'default'
|
||||
DEFAULT_CONFIG = { 'match_filter' => 'state:in_progress' }.freeze
|
||||
DISPLAY_STATES = %w[
|
||||
during_game between_games static_image group_tables
|
||||
].freeze
|
||||
|
|
|
|||
|
|
@ -67,27 +67,32 @@ class Tournament < ApplicationRecord
|
|||
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?
|
||||
return if sync_configuration_blank?
|
||||
return if sync_configuration_complete?
|
||||
return if follower_sync_token_only?
|
||||
|
||||
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
|
||||
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!
|
||||
beamers.find_or_create_by!(name: Beamer::DEFAULT_NAME) do |beamer|
|
||||
beamer.display_state = 'during_game'
|
||||
beamer.config = Beamer::DEFAULT_CONFIG
|
||||
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?
|
||||
|
|
|
|||
|
|
@ -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,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2026_04_26_120000) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2026_04_26_121000) do
|
||||
create_table "beamers", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "display_state", default: "during_game", null: false
|
||||
|
|
|
|||
|
|
@ -38,20 +38,26 @@ RSpec.describe 'Tournament beamer HTTP E2E' do
|
|||
|
||||
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('matches_all')
|
||||
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: 'groups',
|
||||
qr_code_url: 'https://example.com/hall-a',
|
||||
logo_url: '/logos/hall-a.png'
|
||||
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('groups')
|
||||
expect(beamer.fetch(:qr_code_url)).to eq('https://example.com/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)}")
|
||||
|
|
@ -65,34 +71,41 @@ RSpec.describe 'Tournament beamer HTTP E2E' do
|
|||
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: 'during_game',
|
||||
qr_code_url: 'https://example.com/live',
|
||||
logo_url: '/logos/live.png'
|
||||
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('during_game')
|
||||
expect(update_response.dig(:json, :qr_code_url)).to eq('https://example.com/live')
|
||||
expect(update_response.dig(:json, :logo_url)).to eq('/logos/live.png')
|
||||
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: 'matches_all'
|
||||
display_state: 'during_game'
|
||||
})
|
||||
forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||
display_state: 'groups'
|
||||
display_state: 'group_tables'
|
||||
})
|
||||
unauthenticated_create = anonymous_client.post(
|
||||
"/tournaments/#{tournament.fetch(:id)}/beamers",
|
||||
body: {
|
||||
name: 'anon',
|
||||
display_state: 'groups'
|
||||
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)
|
||||
|
|
@ -108,7 +121,7 @@ RSpec.describe 'Tournament beamer HTTP E2E' do
|
|||
)
|
||||
locked_update = owner_client.patch(
|
||||
beamer_path(read_only_tournament.fetch(:id), read_only_beamer.dig(:json, :id)),
|
||||
body: { display_state: 'groups' }
|
||||
body: { display_state: 'group_tables' }
|
||||
)
|
||||
expect(locked_update[:status]).to eq(423)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
|||
follower_default = wait_for_follower_beamer!(
|
||||
tournament_id: follower.fetch(:id),
|
||||
beamer_name: 'default',
|
||||
display_state: source_default.fetch(:display_state)
|
||||
display_state: source_default.fetch(:display_state),
|
||||
config: source_default.fetch(:config)
|
||||
)
|
||||
expect(follower_default.fetch(:name)).to eq('default')
|
||||
|
||||
|
|
@ -49,8 +50,10 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
|||
beamer_path(source.fetch(:id), source_default.fetch(:id)),
|
||||
body: {
|
||||
display_state: 'between_games',
|
||||
qr_code_url: 'https://example.com/leader-default',
|
||||
logo_url: '/logos/default.png'
|
||||
config: {
|
||||
match_filter: 'state:queued',
|
||||
rotation_seconds: 10
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(leader_default_update[:status]).to eq(200)
|
||||
|
|
@ -60,16 +63,22 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
|||
tournament_id: follower.fetch(:id),
|
||||
beamer_name: 'default',
|
||||
display_state: 'between_games',
|
||||
qr_code_url: 'https://example.com/leader-default',
|
||||
logo_url: '/logos/default.png'
|
||||
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: 'groups',
|
||||
qr_code_url: 'https://example.com/stage-left',
|
||||
logo_url: '/logos/stage-left.png'
|
||||
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))
|
||||
|
|
@ -77,17 +86,23 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
|||
mirrored_created = wait_for_follower_beamer!(
|
||||
tournament_id: follower.fetch(:id),
|
||||
beamer_name: 'stage-left',
|
||||
display_state: 'groups',
|
||||
qr_code_url: 'https://example.com/stage-left',
|
||||
logo_url: '/logos/stage-left.png'
|
||||
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: 'during_game',
|
||||
qr_code_url: 'https://example.com/stage-left-live'
|
||||
display_state: 'static_image',
|
||||
config: {
|
||||
image_key: 'stage-left-live'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(leader_update[:status]).to eq(200)
|
||||
|
|
@ -96,14 +111,17 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
|
|||
mirrored_updated = wait_for_follower_beamer!(
|
||||
tournament_id: follower.fetch(:id),
|
||||
beamer_name: 'stage-left',
|
||||
display_state: 'during_game',
|
||||
qr_code_url: 'https://example.com/stage-left-live'
|
||||
display_state: 'static_image',
|
||||
config: {
|
||||
image_key: 'stage-left-live'
|
||||
}
|
||||
)
|
||||
expect(mirrored_updated.fetch(:logo_url)).to eq('/logos/stage-left.png')
|
||||
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: 'matches_finished' }
|
||||
body: { display_state: 'during_game' }
|
||||
)
|
||||
expect(locked_follower_update[:status]).to eq(423)
|
||||
|
||||
|
|
@ -150,7 +168,7 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' 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.fetch(key) == value }
|
||||
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
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ FactoryBot.define do
|
|||
|
||||
factory :default_beamer do
|
||||
name { Beamer::DEFAULT_NAME }
|
||||
config { Beamer::DEFAULT_CONFIG }
|
||||
|
||||
initialize_with do
|
||||
tournament.beamers.find_or_initialize_by(name: name)
|
||||
tournament.beamers.find_or_initialize_by(name:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,8 +35,11 @@ RSpec.describe Tournament, type: :model do
|
|||
describe 'default beamer' do
|
||||
it 'creates a default beamer automatically' do
|
||||
tournament = create(:tournament)
|
||||
default_beamer = tournament.beamers.find_by!(name: 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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue