test: update beamer e2e contract

This commit is contained in:
Daniel Schädler 2026-04-26 23:45:35 +02:00
parent 988171097c
commit 68b72cc01a
7 changed files with 93 additions and 46 deletions

View File

@ -2,6 +2,7 @@
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[
during_game between_games static_image group_tables during_game between_games static_image group_tables
].freeze ].freeze

View File

@ -67,27 +67,32 @@ 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 = 'during_game' beamer.display_state = 'during_game'
beamer.config = Beamer::DEFAULT_CONFIG
end end
end end
def sync_configuration_is_complete
return if sync_target_url.blank? && sync_auth_token.blank?
return if sync_target_url.present? && sync_auth_token.present?
return if read_only_mode? && sync_auth_token.present? && sync_target_url.blank?
errors.add(:base, 'sync_target_url and sync_auth_token must be configured together')
end
def clear_follower_sync_token_when_disabling_read_only_mode def clear_follower_sync_token_when_disabling_read_only_mode
return if read_only_mode? return if read_only_mode?
return if sync_target_url.present? return if sync_target_url.present?

View File

@ -14,6 +14,12 @@ class ReplaceBeamerAssetFieldsWithConfig < ActiveRecord::Migration[7.0]
END END
SQL SQL
execute <<~SQL.squish
UPDATE beamers
SET config = '{"match_filter":"state:in_progress"}'
WHERE name = 'default'
SQL
change_column_default :beamers, :display_state, from: 'matches_all', to: 'during_game' change_column_default :beamers, :display_state, from: 'matches_all', to: 'during_game'
remove_column :beamers, :qr_code_url, :string remove_column :beamers, :qr_code_url, :string
remove_column :beamers, :logo_url, :string remove_column :beamers, :logo_url, :string

View File

@ -38,20 +38,26 @@ RSpec.describe 'Tournament beamer HTTP E2E' do
expect(default_beamer[:status]).to eq(200) expect(default_beamer[:status]).to eq(200)
expect(default_beamer.dig(:json, :name)).to eq('default') 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[: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: { create_response = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
name: 'hall-a', name: 'hall-a',
display_state: 'groups', display_state: 'group_tables',
qr_code_url: 'https://example.com/hall-a', config: {
logo_url: '/logos/hall-a.png' match_filter: 'stage:groups',
table_view: 'compact'
}
}) })
expect(create_response[:status]).to eq(201) expect(create_response[:status]).to eq(201)
beamer = create_response.fetch(:json) beamer = create_response.fetch(:json)
expect(beamer.fetch(:name)).to eq('hall-a') expect(beamer.fetch(:name)).to eq('hall-a')
expect(beamer.fetch(:display_state)).to eq('groups') expect(beamer.fetch(:display_state)).to eq('group_tables')
expect(beamer.fetch(:qr_code_url)).to eq('https://example.com/hall-a') 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") index_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers")
show_response = anonymous_client.get("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}") 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)) 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: { update_response = owner_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/live', config: {
logo_url: '/logos/live.png' image_key: 'bpwstr_logo'
}
}) })
expect(update_response[:status]).to eq(200) expect(update_response[:status]).to eq(200)
expect(update_response.dig(:json, :display_state)).to eq('during_game') expect(update_response.dig(:json, :display_state)).to eq('static_image')
expect(update_response.dig(:json, :qr_code_url)).to eq('https://example.com/live') expect(update_response.dig(:json, :config)).to eq(image_key: 'bpwstr_logo')
expect(update_response.dig(:json, :logo_url)).to eq('/logos/live.png') 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: { invalid_update = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
display_state: 'bogus' 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: { duplicate_create = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
name: 'hall-a', 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: { 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( unauthenticated_create = anonymous_client.post(
"/tournaments/#{tournament.fetch(:id)}/beamers", "/tournaments/#{tournament.fetch(:id)}/beamers",
body: { body: {
name: 'anon', name: 'anon',
display_state: 'groups' display_state: 'group_tables'
} }
) )
expect(invalid_update[:status]).to eq(422) expect(invalid_update[:status]).to eq(422)
expect(invalid_config_update[:status]).to eq(422)
expect(duplicate_create[:status]).to eq(422) expect(duplicate_create[:status]).to eq(422)
expect(forbidden_update[:status]).to eq(403) expect(forbidden_update[:status]).to eq(403)
expect(unauthenticated_create[:status]).to eq(401) expect(unauthenticated_create[:status]).to eq(401)
@ -108,7 +121,7 @@ RSpec.describe 'Tournament beamer HTTP E2E' do
) )
locked_update = owner_client.patch( locked_update = owner_client.patch(
beamer_path(read_only_tournament.fetch(:id), read_only_beamer.dig(:json, :id)), 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) expect(locked_update[:status]).to eq(423)

View File

@ -41,7 +41,8 @@ RSpec.describe 'Tournament follower sync beamer HTTP E2E' do
follower_default = wait_for_follower_beamer!( follower_default = wait_for_follower_beamer!(
tournament_id: follower.fetch(:id), tournament_id: follower.fetch(:id),
beamer_name: 'default', 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') 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)), beamer_path(source.fetch(:id), source_default.fetch(:id)),
body: { body: {
display_state: 'between_games', display_state: 'between_games',
qr_code_url: 'https://example.com/leader-default', config: {
logo_url: '/logos/default.png' match_filter: 'state:queued',
rotation_seconds: 10
}
} }
) )
expect(leader_default_update[:status]).to eq(200) 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), tournament_id: follower.fetch(:id),
beamer_name: 'default', beamer_name: 'default',
display_state: 'between_games', display_state: 'between_games',
qr_code_url: 'https://example.com/leader-default', config: {
logo_url: '/logos/default.png' match_filter: 'state:queued',
rotation_seconds: 10
}
) )
expect(mirrored_default.fetch(:name)).to eq('default') 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: { leader_create = source_client.post("/tournaments/#{source.fetch(:id)}/beamers", body: {
name: 'stage-left', name: 'stage-left',
display_state: 'groups', display_state: 'group_tables',
qr_code_url: 'https://example.com/stage-left', config: {
logo_url: '/logos/stage-left.png' match_filter: 'stage:groups',
table_view: 'compact'
}
}) })
expect(leader_create[:status]).to eq(201) expect(leader_create[:status]).to eq(201)
sync_now_if_requested!(source.fetch(:id)) 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!( mirrored_created = wait_for_follower_beamer!(
tournament_id: follower.fetch(:id), tournament_id: follower.fetch(:id),
beamer_name: 'stage-left', beamer_name: 'stage-left',
display_state: 'groups', display_state: 'group_tables',
qr_code_url: 'https://example.com/stage-left', config: {
logo_url: '/logos/stage-left.png' match_filter: 'stage:groups',
table_view: 'compact'
}
) )
expect(mirrored_created.fetch(:name)).to eq('stage-left') 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( leader_update = source_client.patch(
beamer_path(source.fetch(:id), leader_create.dig(:json, :id)), beamer_path(source.fetch(:id), leader_create.dig(:json, :id)),
body: { body: {
display_state: 'during_game', display_state: 'static_image',
qr_code_url: 'https://example.com/stage-left-live' config: {
image_key: 'stage-left-live'
}
} }
) )
expect(leader_update[:status]).to eq(200) 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!( mirrored_updated = wait_for_follower_beamer!(
tournament_id: follower.fetch(:id), tournament_id: follower.fetch(:id),
beamer_name: 'stage-left', beamer_name: 'stage-left',
display_state: 'during_game', display_state: 'static_image',
qr_code_url: 'https://example.com/stage-left-live' 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( locked_follower_update = follower_client.patch(
beamer_path(follower.fetch(:id), mirrored_updated.fetch(:id)), 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) 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 }) 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 if response[:status] == 200 && response.dig(:json, :name) == beamer_name
beamer = response.fetch(:json) 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 end
raise "follower beamer #{beamer_name} not synced within #{timeout}s" if Time.now.utc >= deadline raise "follower beamer #{beamer_name} not synced within #{timeout}s" if Time.now.utc >= deadline

View File

@ -9,9 +9,10 @@ FactoryBot.define do
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: name) tournament.beamers.find_or_initialize_by(name:)
end end
end end
end end

View File

@ -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