feat(tournaments): add metadata fields
Add typed tournament metadata to API payloads and follower sync.
This commit is contained in:
parent
547e1c3933
commit
9b10cc12d2
|
|
@ -285,6 +285,7 @@ class TournamentsController < ApplicationController
|
||||||
params.slice(
|
params.slice(
|
||||||
:name, :description, :public, :teams, :group_stage, :playoff_teams_amount,
|
:name, :description, :public, :teams, :group_stage, :playoff_teams_amount,
|
||||||
:instant_finalists_amount, :intermediate_round_participants_amount,
|
:instant_finalists_amount, :intermediate_round_participants_amount,
|
||||||
|
:location, :starts_at, :ends_at, :organizer_name, :contact_email, :website_url,
|
||||||
:read_only_mode, :sync_target_url, :sync_auth_token
|
:read_only_mode, :sync_target_url, :sync_auth_token
|
||||||
).permit!
|
).permit!
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ require 'openssl'
|
||||||
class Tournament < ApplicationRecord
|
class Tournament < ApplicationRecord
|
||||||
TIMER_MODES = %w[countdown countup].freeze
|
TIMER_MODES = %w[countdown countup].freeze
|
||||||
TIMER_REASONS = %w[tournament_start playoff_start match_end custom].freeze
|
TIMER_REASONS = %w[tournament_start playoff_start match_end custom].freeze
|
||||||
|
METADATA_STRING_ATTRIBUTES = %i[location organizer_name contact_email website_url].freeze
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :owner, class_name: 'User', foreign_key: :user_id
|
belongs_to :owner, class_name: 'User', foreign_key: :user_id
|
||||||
|
|
@ -20,14 +21,18 @@ class Tournament < ApplicationRecord
|
||||||
validates :timer_mode, inclusion: { in: TIMER_MODES }, allow_nil: true
|
validates :timer_mode, inclusion: { in: TIMER_MODES }, allow_nil: true
|
||||||
validates :timer_reason, inclusion: { in: TIMER_REASONS }, allow_nil: true
|
validates :timer_reason, inclusion: { in: TIMER_REASONS }, allow_nil: true
|
||||||
validates :sync_target_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true
|
validates :sync_target_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true
|
||||||
|
validates :website_url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true
|
||||||
|
validates :contact_email, format: URI::MailTo::EMAIL_REGEXP, allow_blank: true
|
||||||
validate :sync_configuration_is_complete
|
validate :sync_configuration_is_complete
|
||||||
|
|
||||||
validate :playoff_teams_amount_is_positive_power_of_two
|
validate :playoff_teams_amount_is_positive_power_of_two
|
||||||
validate :timer_reason_text_matches_reason
|
validate :timer_reason_text_matches_reason
|
||||||
|
validate :ends_at_is_after_starts_at
|
||||||
|
|
||||||
after_initialize :generate_code
|
after_initialize :generate_code
|
||||||
after_create_commit :ensure_default_beamer!
|
after_create_commit :ensure_default_beamer!
|
||||||
before_validation :normalize_timer_reason
|
before_validation :normalize_timer_reason
|
||||||
|
before_validation :normalize_metadata
|
||||||
before_validation :protect_follower_sync_token
|
before_validation :protect_follower_sync_token
|
||||||
before_validation :clear_follower_sync_token_when_disabling_read_only_mode
|
before_validation :clear_follower_sync_token_when_disabling_read_only_mode
|
||||||
after_commit :broadcast_timer_state_change, if: :saved_change_to_timer_state?
|
after_commit :broadcast_timer_state_change, if: :saved_change_to_timer_state?
|
||||||
|
|
@ -154,6 +159,19 @@ class Tournament < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_metadata
|
||||||
|
METADATA_STRING_ATTRIBUTES.each do |attribute|
|
||||||
|
self[attribute] = self[attribute]&.strip&.presence
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ends_at_is_after_starts_at
|
||||||
|
return if starts_at.blank? || ends_at.blank?
|
||||||
|
return if ends_at > starts_at
|
||||||
|
|
||||||
|
errors.add(:ends_at, 'must be after starts_at')
|
||||||
|
end
|
||||||
|
|
||||||
def saved_change_to_timer_state?
|
def saved_change_to_timer_state?
|
||||||
saved_change_to_timestamp? ||
|
saved_change_to_timestamp? ||
|
||||||
saved_change_to_timer_mode? ||
|
saved_change_to_timer_mode? ||
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SimpleTournamentSerializer < ApplicationSerializer
|
class SimpleTournamentSerializer < ApplicationSerializer
|
||||||
attributes :name, :code, :public
|
attributes :name, :code, :public, :location, :organizer_name, :contact_email, :website_url
|
||||||
|
|
||||||
|
attribute :starts_at do
|
||||||
|
object.starts_at&.iso8601
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute :ends_at do
|
||||||
|
object.ends_at&.iso8601
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class TournamentSnapshotBuilder
|
||||||
|
|
||||||
def build
|
def build
|
||||||
payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES)
|
payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES)
|
||||||
|
payload[:starts_at] = tournament.starts_at&.iso8601
|
||||||
|
payload[:ends_at] = tournament.ends_at&.iso8601
|
||||||
payload[:timestamp] = tournament.timestamp&.iso8601
|
payload[:timestamp] = tournament.timestamp&.iso8601
|
||||||
payload[:timer_mode] = tournament.timer_mode
|
payload[:timer_mode] = tournament.timer_mode
|
||||||
payload[:timer_reason] = tournament.timer_reason
|
payload[:timer_reason] = tournament.timer_reason
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ class TournamentSnapshotImporter
|
||||||
code
|
code
|
||||||
description
|
description
|
||||||
public
|
public
|
||||||
|
location
|
||||||
|
organizer_name
|
||||||
|
contact_email
|
||||||
|
website_url
|
||||||
playoff_teams_amount
|
playoff_teams_amount
|
||||||
instant_finalists_amount
|
instant_finalists_amount
|
||||||
intermediate_round_participants_amount
|
intermediate_round_participants_amount
|
||||||
|
|
@ -67,6 +71,8 @@ class TournamentSnapshotImporter
|
||||||
def update_tournament!
|
def update_tournament!
|
||||||
root_attrs = snapshot.slice(*ROOT_ATTRIBUTES)
|
root_attrs = snapshot.slice(*ROOT_ATTRIBUTES)
|
||||||
root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code])
|
root_attrs[:code] = tournament.code if code_taken_by_another_tournament?(root_attrs[:code])
|
||||||
|
root_attrs[:starts_at] = parse_time(snapshot[:starts_at])
|
||||||
|
root_attrs[:ends_at] = parse_time(snapshot[:ends_at])
|
||||||
root_attrs[:timestamp] = parse_time(snapshot[:timestamp])
|
root_attrs[:timestamp] = parse_time(snapshot[:timestamp])
|
||||||
root_attrs[:timer_mode] = snapshot[:timer_mode]
|
root_attrs[:timer_mode] = snapshot[:timer_mode]
|
||||||
root_attrs[:timer_reason] = snapshot[:timer_reason]
|
root_attrs[:timer_reason] = snapshot[:timer_reason]
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ class TournamentSyncSchema
|
||||||
code
|
code
|
||||||
description
|
description
|
||||||
public
|
public
|
||||||
|
location
|
||||||
|
starts_at
|
||||||
|
ends_at
|
||||||
|
organizer_name
|
||||||
|
contact_email
|
||||||
|
website_url
|
||||||
playoff_teams_amount
|
playoff_teams_amount
|
||||||
instant_finalists_amount
|
instant_finalists_amount
|
||||||
intermediate_round_participants_amount
|
intermediate_round_participants_amount
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddMetadataToTournaments < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
change_table :tournaments, bulk: true do |t|
|
||||||
|
t.string :location
|
||||||
|
t.datetime :starts_at
|
||||||
|
t.datetime :ends_at
|
||||||
|
t.string :organizer_name
|
||||||
|
t.string :contact_email
|
||||||
|
t.string :website_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# 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[8.1].define(version: 2026_04_30_121000) do
|
ActiveRecord::Schema[8.1].define(version: 2026_05_04_100000) do
|
||||||
create_table "beamers", force: :cascade do |t|
|
create_table "beamers", force: :cascade do |t|
|
||||||
t.json "config", default: {}, null: false
|
t.json "config", default: {}, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|
@ -163,14 +163,19 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
||||||
|
|
||||||
create_table "tournaments", force: :cascade do |t|
|
create_table "tournaments", force: :cascade do |t|
|
||||||
t.string "code", null: false
|
t.string "code", null: false
|
||||||
|
t.string "contact_email"
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.string "description"
|
t.string "description"
|
||||||
|
t.datetime "ends_at"
|
||||||
t.integer "instant_finalists_amount", default: 0
|
t.integer "instant_finalists_amount", default: 0
|
||||||
t.integer "intermediate_round_participants_amount", default: 0
|
t.integer "intermediate_round_participants_amount", default: 0
|
||||||
|
t.string "location"
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
|
t.string "organizer_name"
|
||||||
t.integer "playoff_teams_amount", default: 0
|
t.integer "playoff_teams_amount", default: 0
|
||||||
t.boolean "public", default: true
|
t.boolean "public", default: true
|
||||||
t.boolean "read_only_mode", default: false, null: false
|
t.boolean "read_only_mode", default: false, null: false
|
||||||
|
t.datetime "starts_at"
|
||||||
t.string "sync_auth_token"
|
t.string "sync_auth_token"
|
||||||
t.string "sync_auth_token_digest"
|
t.string "sync_auth_token_digest"
|
||||||
t.datetime "sync_last_imported_snapshot_at"
|
t.datetime "sync_last_imported_snapshot_at"
|
||||||
|
|
@ -184,6 +189,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
||||||
t.datetime "timestamp"
|
t.datetime "timestamp"
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
t.integer "user_id", null: false
|
t.integer "user_id", null: false
|
||||||
|
t.string "website_url"
|
||||||
t.index ["code"], name: "index_tournaments_on_code", unique: true
|
t.index ["code"], name: "index_tournaments_on_code", unique: true
|
||||||
t.index ["user_id"], name: "index_tournaments_on_user_id"
|
t.index ["user_id"], name: "index_tournaments_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
expected_keys = %i[
|
expected_keys = %i[
|
||||||
id name code public description playoff_teams_amount
|
id name code public description playoff_teams_amount
|
||||||
instant_finalists_amount intermediate_round_participants_amount
|
instant_finalists_amount intermediate_round_participants_amount
|
||||||
|
location starts_at ends_at organizer_name contact_email website_url
|
||||||
timestamp timer_mode timer_reason timer_reason_text owner_username stages teams team_action_lists
|
timestamp timer_mode timer_reason timer_reason_text owner_username stages teams team_action_lists
|
||||||
]
|
]
|
||||||
expect(json.keys).to match_array(expected_keys)
|
expect(json.keys).to match_array(expected_keys)
|
||||||
|
|
@ -407,6 +408,29 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
end.to change(Tournament, :count).by(1)
|
end.to change(Tournament, :count).by(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'stores tournament metadata' do
|
||||||
|
starts_at = '2030-06-01T10:00:00+02:00'
|
||||||
|
ends_at = '2030-06-01T18:00:00+02:00'
|
||||||
|
|
||||||
|
post :create, params: create_playoff_tournament_data.merge(
|
||||||
|
location: 'Stadtpark Halle',
|
||||||
|
starts_at: starts_at,
|
||||||
|
ends_at: ends_at,
|
||||||
|
organizer_name: 'BPWSTR Crew',
|
||||||
|
contact_email: 'orga@example.com',
|
||||||
|
website_url: 'https://example.com/tournaments/summer-cup'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
tournament = Tournament.find(deserialize_response(response)[:id])
|
||||||
|
expect(tournament.location).to eq('Stadtpark Halle')
|
||||||
|
expect(tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601)
|
||||||
|
expect(tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601)
|
||||||
|
expect(tournament.organizer_name).to eq('BPWSTR Crew')
|
||||||
|
expect(tournament.contact_email).to eq('orga@example.com')
|
||||||
|
expect(tournament.website_url).to eq('https://example.com/tournaments/summer-cup')
|
||||||
|
end
|
||||||
|
|
||||||
it 'associates the new tournament with the authenticated user' do
|
it 'associates the new tournament with the authenticated user' do
|
||||||
expect do
|
expect do
|
||||||
post :create, params: create_playoff_tournament_data
|
post :create, params: create_playoff_tournament_data
|
||||||
|
|
@ -587,6 +611,49 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
expect(@tournament.name).to eq(valid_update[:name])
|
expect(@tournament.name).to eq(valid_update[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates tournament metadata' do
|
||||||
|
starts_at = '2030-06-01T10:00:00+02:00'
|
||||||
|
ends_at = '2030-06-01T18:00:00+02:00'
|
||||||
|
|
||||||
|
put :update, params: {
|
||||||
|
id: @tournament.to_param,
|
||||||
|
location: 'Arena Nord',
|
||||||
|
starts_at: starts_at,
|
||||||
|
ends_at: ends_at,
|
||||||
|
organizer_name: 'Turniere Ops',
|
||||||
|
contact_email: 'ops@example.com',
|
||||||
|
website_url: 'https://example.com/events/arena-nord'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
@tournament.reload
|
||||||
|
json = deserialize_response(response)
|
||||||
|
expect(@tournament.location).to eq('Arena Nord')
|
||||||
|
expect(@tournament.starts_at.iso8601).to eq(Time.iso8601(starts_at).utc.iso8601)
|
||||||
|
expect(@tournament.ends_at.iso8601).to eq(Time.iso8601(ends_at).utc.iso8601)
|
||||||
|
expect(@tournament.organizer_name).to eq('Turniere Ops')
|
||||||
|
expect(@tournament.contact_email).to eq('ops@example.com')
|
||||||
|
expect(@tournament.website_url).to eq('https://example.com/events/arena-nord')
|
||||||
|
expect(json[:starts_at]).to eq(@tournament.starts_at.iso8601)
|
||||||
|
expect(json[:ends_at]).to eq(@tournament.ends_at.iso8601)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects invalid tournament metadata' do
|
||||||
|
put :update, params: {
|
||||||
|
id: @tournament.to_param,
|
||||||
|
starts_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
contact_email: 'not-an-email',
|
||||||
|
website_url: 'notaurl'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_content)
|
||||||
|
json = deserialize_response(response)
|
||||||
|
expect(json[:ends_at]).to include('must be after starts_at')
|
||||||
|
expect(json[:contact_email]).to include('is invalid')
|
||||||
|
expect(json[:website_url]).to include('is invalid')
|
||||||
|
end
|
||||||
|
|
||||||
it 'renders a JSON response with the tournament' do
|
it 'renders a JSON response with the tournament' do
|
||||||
put :update, params: { id: @tournament.to_param }.merge(valid_update)
|
put :update, params: { id: @tournament.to_param }.merge(valid_update)
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
@ -810,6 +877,12 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
code: 'abc123',
|
code: 'abc123',
|
||||||
description: 'Synced description',
|
description: 'Synced description',
|
||||||
public: true,
|
public: true,
|
||||||
|
location: 'Synced Arena',
|
||||||
|
starts_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Synced Crew',
|
||||||
|
contact_email: 'sync@example.com',
|
||||||
|
website_url: 'https://example.com/synced',
|
||||||
playoff_teams_amount: 4,
|
playoff_teams_amount: 4,
|
||||||
instant_finalists_amount: 4,
|
instant_finalists_amount: 4,
|
||||||
intermediate_round_participants_amount: 0,
|
intermediate_round_participants_amount: 0,
|
||||||
|
|
@ -827,6 +900,12 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
@tournament.reload
|
@tournament.reload
|
||||||
expect(@tournament.sync_source_tournament_id).to eq(123)
|
expect(@tournament.sync_source_tournament_id).to eq(123)
|
||||||
expect(@tournament.name).to eq('Synced Tournament')
|
expect(@tournament.name).to eq('Synced Tournament')
|
||||||
|
expect(@tournament.location).to eq('Synced Arena')
|
||||||
|
expect(@tournament.starts_at.iso8601).to eq(Time.iso8601('2030-06-01T10:00:00+02:00').utc.iso8601)
|
||||||
|
expect(@tournament.ends_at.iso8601).to eq(Time.iso8601('2030-06-01T18:00:00+02:00').utc.iso8601)
|
||||||
|
expect(@tournament.organizer_name).to eq('Synced Crew')
|
||||||
|
expect(@tournament.contact_email).to eq('sync@example.com')
|
||||||
|
expect(@tournament.website_url).to eq('https://example.com/synced')
|
||||||
expect(@tournament.teams.pluck(:name)).to eq(['Alpha'])
|
expect(@tournament.teams.pluck(:name)).to eq(['Alpha'])
|
||||||
expect(deserialize_response(response)).not_to have_key(:sync_auth_token)
|
expect(deserialize_response(response)).not_to have_key(:sync_auth_token)
|
||||||
expect(deserialize_response(response)).not_to have_key(:sync_auth_token_digest)
|
expect(deserialize_response(response)).not_to have_key(:sync_auth_token_digest)
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(full_show[:status]).to eq(200)
|
expect(full_show[:status]).to eq(200)
|
||||||
expect(full_show.dig(:json, :stages)).to be_an(Array)
|
expect(full_show.dig(:json, :stages)).to be_an(Array)
|
||||||
expect(full_show.dig(:json, :teams)).to be_an(Array)
|
expect(full_show.dig(:json, :teams)).to be_an(Array)
|
||||||
|
expect(full_show.dig(:json, :location)).to start_with('Venue')
|
||||||
|
expect(full_show.dig(:json, :starts_at)).to eq('2030-06-01T08:00:00Z')
|
||||||
|
expect(full_show.dig(:json, :ends_at)).to eq('2030-06-01T16:00:00Z')
|
||||||
|
expect(full_show.dig(:json, :organizer_name)).to eq('Turniere E2E')
|
||||||
|
expect(full_show.dig(:json, :contact_email)).to eq('events@example.com')
|
||||||
|
expect(full_show.dig(:json, :website_url)).to eq('https://example.com/turniere-e2e')
|
||||||
|
|
||||||
expect(invalid_index[:status]).to eq(400)
|
expect(invalid_index[:status]).to eq(400)
|
||||||
expect(invalid_index.dig(:json, :error)).to eq('invalid type')
|
expect(invalid_index.dig(:json, :error)).to eq('invalid type')
|
||||||
|
|
@ -119,6 +125,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
|
|
||||||
update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: {
|
update_response = owner_client.patch("/tournaments/#{tournament.fetch(:id)}", body: {
|
||||||
name: "Renamed #{unique_suffix}",
|
name: "Renamed #{unique_suffix}",
|
||||||
|
location: 'Updated E2E Arena',
|
||||||
|
starts_at: '2030-07-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-07-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Updated E2E Org',
|
||||||
|
contact_email: 'updated-events@example.com',
|
||||||
|
website_url: 'https://example.com/updated-turniere-e2e',
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
instant_finalists_amount: 8,
|
instant_finalists_amount: 8,
|
||||||
intermediate_round_participants_amount: 0
|
intermediate_round_participants_amount: 0
|
||||||
|
|
@ -172,6 +184,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
|
|
||||||
expect(update_response[:status]).to eq(200)
|
expect(update_response[:status]).to eq(200)
|
||||||
expect(update_response.dig(:json, :name)).to start_with('Renamed')
|
expect(update_response.dig(:json, :name)).to start_with('Renamed')
|
||||||
|
expect(update_response.dig(:json, :location)).to eq('Updated E2E Arena')
|
||||||
|
expect(update_response.dig(:json, :starts_at)).to eq('2030-07-01T08:00:00Z')
|
||||||
|
expect(update_response.dig(:json, :ends_at)).to eq('2030-07-01T16:00:00Z')
|
||||||
|
expect(update_response.dig(:json, :organizer_name)).to eq('Updated E2E Org')
|
||||||
|
expect(update_response.dig(:json, :contact_email)).to eq('updated-events@example.com')
|
||||||
|
expect(update_response.dig(:json, :website_url)).to eq('https://example.com/updated-turniere-e2e')
|
||||||
|
|
||||||
expect(auto_recalc_response[:status]).to eq(200)
|
expect(auto_recalc_response[:status]).to eq(200)
|
||||||
expect(auto_recalc_response.dig(:json, :playoff_teams_amount)).to eq(4)
|
expect(auto_recalc_response.dig(:json, :playoff_teams_amount)).to eq(4)
|
||||||
|
|
@ -511,6 +529,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'HTTP API surface E2E group-stage tournament',
|
description: 'HTTP API surface E2E group-stage tournament',
|
||||||
public: public,
|
public: public,
|
||||||
|
location: "Venue #{name_prefix}",
|
||||||
|
starts_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Turniere E2E',
|
||||||
|
contact_email: 'events@example.com',
|
||||||
|
website_url: 'https://example.com/turniere-e2e',
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
teams: 4.times.flat_map do |group_index|
|
teams: 4.times.flat_map do |group_index|
|
||||||
|
|
@ -567,6 +591,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'HTTP API surface E2E playoff tournament',
|
description: 'HTTP API surface E2E playoff tournament',
|
||||||
public: public,
|
public: public,
|
||||||
|
location: "Venue #{name_prefix}",
|
||||||
|
starts_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Turniere E2E',
|
||||||
|
contact_email: 'events@example.com',
|
||||||
|
website_url: 'https://example.com/turniere-e2e',
|
||||||
teams: 4.times.map do |index|
|
teams: 4.times.map do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
|
|
||||||
log_phase!('tighten playoff cutoff and rename teams')
|
log_phase!('tighten playoff cutoff and rename teams')
|
||||||
update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
update_cutoff = source_owner_client.patch("/tournaments/#{source.fetch(:id)}", body: {
|
||||||
|
location: 'Source Updated Arena',
|
||||||
|
starts_at: '2030-07-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-07-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Updated Sync Crew',
|
||||||
|
contact_email: 'updated-sync@example.com',
|
||||||
|
website_url: 'https://example.com/updated-sync-source',
|
||||||
playoff_teams_amount: 4,
|
playoff_teams_amount: 4,
|
||||||
instant_finalists_amount: 4,
|
instant_finalists_amount: 4,
|
||||||
intermediate_round_participants_amount: 0
|
intermediate_round_participants_amount: 0
|
||||||
|
|
@ -233,6 +239,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Follower sync source tournament',
|
description: 'Follower sync source tournament',
|
||||||
public: public,
|
public: public,
|
||||||
|
location: "Source Venue #{name_prefix}",
|
||||||
|
starts_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Sync Source Crew',
|
||||||
|
contact_email: 'sync-source@example.com',
|
||||||
|
website_url: 'https://example.com/sync-source',
|
||||||
group_stage: true,
|
group_stage: true,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
teams: 4.times.flat_map do |group_index|
|
teams: 4.times.flat_map do |group_index|
|
||||||
|
|
@ -255,6 +267,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Unrelated playoff tournament',
|
description: 'Unrelated playoff tournament',
|
||||||
public: public,
|
public: public,
|
||||||
|
location: "Noise Venue #{name_prefix}",
|
||||||
|
starts_at: '2030-06-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-06-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Noise Crew',
|
||||||
|
contact_email: 'noise@example.com',
|
||||||
|
website_url: 'https://example.com/noise',
|
||||||
teams: 4.times.map do |index|
|
teams: 4.times.map do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
|
|
@ -270,6 +288,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
name: "#{name_prefix} #{unique_suffix}",
|
name: "#{name_prefix} #{unique_suffix}",
|
||||||
description: 'Follower tournament',
|
description: 'Follower tournament',
|
||||||
public: public,
|
public: public,
|
||||||
|
location: "Follower Venue #{name_prefix}",
|
||||||
|
starts_at: '2030-05-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-05-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Follower Crew',
|
||||||
|
contact_email: 'follower@example.com',
|
||||||
|
website_url: 'https://example.com/follower',
|
||||||
read_only_mode: true,
|
read_only_mode: true,
|
||||||
sync_auth_token: sync_token
|
sync_auth_token: sync_token
|
||||||
})
|
})
|
||||||
|
|
@ -500,6 +524,12 @@ RSpec.describe 'Tournament follower sync HTTP E2E' do
|
||||||
{
|
{
|
||||||
name: tournament.fetch(:name),
|
name: tournament.fetch(:name),
|
||||||
public: tournament.fetch(:public),
|
public: tournament.fetch(:public),
|
||||||
|
location: tournament[:location],
|
||||||
|
starts_at: tournament[:starts_at],
|
||||||
|
ends_at: tournament[:ends_at],
|
||||||
|
organizer_name: tournament[:organizer_name],
|
||||||
|
contact_email: tournament[:contact_email],
|
||||||
|
website_url: tournament[:website_url],
|
||||||
playoff_teams_amount: tournament.fetch(:playoff_teams_amount),
|
playoff_teams_amount: tournament.fetch(:playoff_teams_amount),
|
||||||
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
instant_finalists_amount: tournament.fetch(:instant_finalists_amount),
|
||||||
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
intermediate_round_participants_amount: tournament.fetch(:intermediate_round_participants_amount),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,13 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
|
|
||||||
rename = client.patch("/tournaments/#{leader.fetch(:id)}", body: {
|
rename = client.patch("/tournaments/#{leader.fetch(:id)}", body: {
|
||||||
name: "Leader Renamed #{SecureRandom.hex(3)}",
|
name: "Leader Renamed #{SecureRandom.hex(3)}",
|
||||||
description: 'Roundtrip sync test'
|
description: 'Roundtrip sync test',
|
||||||
|
location: 'Sync Test Arena',
|
||||||
|
starts_at: '2030-08-01T10:00:00+02:00',
|
||||||
|
ends_at: '2030-08-01T18:00:00+02:00',
|
||||||
|
organizer_name: 'Sync Test Org',
|
||||||
|
contact_email: 'sync-test@example.com',
|
||||||
|
website_url: 'https://example.com/sync-test'
|
||||||
})
|
})
|
||||||
expect(rename[:status]).to eq(200)
|
expect(rename[:status]).to eq(200)
|
||||||
|
|
||||||
|
|
@ -52,6 +58,12 @@ RSpec.describe 'Tournament sync test HTTP E2E' do
|
||||||
|
|
||||||
expect(after_follower.fetch(:name)).to eq(after_leader.fetch(:name))
|
expect(after_follower.fetch(:name)).to eq(after_leader.fetch(:name))
|
||||||
expect(after_follower.fetch(:description)).to eq(after_leader.fetch(:description))
|
expect(after_follower.fetch(:description)).to eq(after_leader.fetch(:description))
|
||||||
|
expect(after_follower.fetch(:location)).to eq(after_leader.fetch(:location))
|
||||||
|
expect(after_follower.fetch(:starts_at)).to eq(after_leader.fetch(:starts_at))
|
||||||
|
expect(after_follower.fetch(:ends_at)).to eq(after_leader.fetch(:ends_at))
|
||||||
|
expect(after_follower.fetch(:organizer_name)).to eq(after_leader.fetch(:organizer_name))
|
||||||
|
expect(after_follower.fetch(:contact_email)).to eq(after_leader.fetch(:contact_email))
|
||||||
|
expect(after_follower.fetch(:website_url)).to eq(after_leader.fetch(:website_url))
|
||||||
expect(after_follower.fetch(:teams).map { |team| team.fetch(:name) })
|
expect(after_follower.fetch(:teams).map { |team| team.fetch(:name) })
|
||||||
.to match_array(after_leader.fetch(:teams).map { |team| team.fetch(:name) })
|
.to match_array(after_leader.fetch(:teams).map { |team| team.fetch(:name) })
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ RSpec.describe TournamentSnapshotImporter 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.update!(
|
source.update!(
|
||||||
|
location: 'Court 4',
|
||||||
|
starts_at: Time.utc(2026, 4, 24, 9, 0, 0),
|
||||||
|
ends_at: Time.utc(2026, 4, 24, 18, 30, 0),
|
||||||
|
organizer_name: 'Sync Ops',
|
||||||
|
contact_email: 'sync-ops@example.com',
|
||||||
|
website_url: 'https://example.com/sync-cup',
|
||||||
timestamp: Time.utc(2026, 4, 24, 10, 15, 0),
|
timestamp: Time.utc(2026, 4, 24, 10, 15, 0),
|
||||||
timer_mode: 'countdown',
|
timer_mode: 'countdown',
|
||||||
timer_reason: 'custom',
|
timer_reason: 'custom',
|
||||||
|
|
@ -28,6 +34,12 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
follower.reload
|
follower.reload
|
||||||
expect(follower.sync_source_tournament_id).to eq(source.id)
|
expect(follower.sync_source_tournament_id).to eq(source.id)
|
||||||
expect(follower.name).to eq(source.name)
|
expect(follower.name).to eq(source.name)
|
||||||
|
expect(follower.location).to eq(source.location)
|
||||||
|
expect(follower.starts_at).to eq(source.starts_at)
|
||||||
|
expect(follower.ends_at).to eq(source.ends_at)
|
||||||
|
expect(follower.organizer_name).to eq(source.organizer_name)
|
||||||
|
expect(follower.contact_email).to eq(source.contact_email)
|
||||||
|
expect(follower.website_url).to eq(source.website_url)
|
||||||
expect(follower.timer_mode).to eq(source.timer_mode)
|
expect(follower.timer_mode).to eq(source.timer_mode)
|
||||||
expect(follower.timer_reason).to eq(source.timer_reason)
|
expect(follower.timer_reason).to eq(source.timer_reason)
|
||||||
expect(follower.timer_reason_text).to eq(source.timer_reason_text)
|
expect(follower.timer_reason_text).to eq(source.timer_reason_text)
|
||||||
|
|
@ -197,6 +209,12 @@ RSpec.describe TournamentSnapshotImporter do
|
||||||
{
|
{
|
||||||
name: tournament.name,
|
name: tournament.name,
|
||||||
public: tournament.public,
|
public: tournament.public,
|
||||||
|
location: tournament.location,
|
||||||
|
starts_at: tournament.starts_at&.iso8601,
|
||||||
|
ends_at: tournament.ends_at&.iso8601,
|
||||||
|
organizer_name: tournament.organizer_name,
|
||||||
|
contact_email: tournament.contact_email,
|
||||||
|
website_url: tournament.website_url,
|
||||||
playoff_teams_amount: tournament.playoff_teams_amount,
|
playoff_teams_amount: tournament.playoff_teams_amount,
|
||||||
instant_finalists_amount: tournament.instant_finalists_amount,
|
instant_finalists_amount: tournament.instant_finalists_amount,
|
||||||
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue