Merge branch 'codex/tur-78-beamer-e2e' into 'master'
TUR-78: E2E tests beamer switching See merge request turniere/turniere-backend!62
This commit is contained in:
commit
2a3c332745
|
|
@ -0,0 +1,154 @@
|
|||
# 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('matches_all')
|
||||
expect(default_show[:status]).to eq(200)
|
||||
|
||||
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'
|
||||
})
|
||||
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')
|
||||
|
||||
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: 'during_game',
|
||||
qr_code_url: 'https://example.com/live',
|
||||
logo_url: '/logos/live.png'
|
||||
})
|
||||
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')
|
||||
|
||||
invalid_update = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||
display_state: 'bogus'
|
||||
})
|
||||
duplicate_create = owner_client.post("/tournaments/#{tournament.fetch(:id)}/beamers", body: {
|
||||
name: 'hall-a',
|
||||
display_state: 'matches_all'
|
||||
})
|
||||
forbidden_update = other_client.patch("/tournaments/#{tournament.fetch(:id)}/beamers/#{beamer.fetch(:id)}", body: {
|
||||
display_state: 'groups'
|
||||
})
|
||||
unauthenticated_create = anonymous_client.post(
|
||||
"/tournaments/#{tournament.fetch(:id)}/beamers",
|
||||
body: {
|
||||
name: 'anon',
|
||||
display_state: 'groups'
|
||||
}
|
||||
)
|
||||
|
||||
expect(invalid_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: 'groups' }
|
||||
)
|
||||
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
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
# 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)
|
||||
)
|
||||
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',
|
||||
qr_code_url: 'https://example.com/leader-default',
|
||||
logo_url: '/logos/default.png'
|
||||
}
|
||||
)
|
||||
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',
|
||||
qr_code_url: 'https://example.com/leader-default',
|
||||
logo_url: '/logos/default.png'
|
||||
)
|
||||
expect(mirrored_default.fetch(:name)).to eq('default')
|
||||
|
||||
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'
|
||||
})
|
||||
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: 'groups',
|
||||
qr_code_url: 'https://example.com/stage-left',
|
||||
logo_url: '/logos/stage-left.png'
|
||||
)
|
||||
expect(mirrored_created.fetch(:name)).to eq('stage-left')
|
||||
|
||||
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'
|
||||
}
|
||||
)
|
||||
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: 'during_game',
|
||||
qr_code_url: 'https://example.com/stage-left-live'
|
||||
)
|
||||
expect(mirrored_updated.fetch(:logo_url)).to eq('/logos/stage-left.png')
|
||||
|
||||
locked_follower_update = follower_client.patch(
|
||||
beamer_path(follower.fetch(:id), mirrored_updated.fetch(:id)),
|
||||
body: { display_state: 'matches_finished' }
|
||||
)
|
||||
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.fetch(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
|
||||
1
tasks.py
1
tasks.py
|
|
@ -1764,6 +1764,7 @@ def blackbox_follow_service(
|
|||
alt_username,
|
||||
spec_path=[
|
||||
"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_live_websocket_spec.rb",
|
||||
"spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb",
|
||||
|
|
|
|||
Loading…
Reference in New Issue