feat(accounts): add player team profiles
Teams can link to player accounts by email while keeping profile tournament data public unless viewed by the account owner.
This commit is contained in:
parent
547e1c3933
commit
995a2cc250
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountsController < ApplicationController
|
||||||
|
before_action :set_account, only: %i[show]
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @account, serializer: AccountProfileSerializer, scope: current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = User.includes(:tournaments, player_teams: :tournament).find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,6 +9,7 @@ class ApplicationController < ActionController::API
|
||||||
rescue_from ActionController::ParameterMissing do |e|
|
rescue_from ActionController::ParameterMissing do |e|
|
||||||
render json: { error: e.message }, status: :bad_request
|
render json: { error: e.message }, status: :bad_request
|
||||||
end
|
end
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid_error
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
@ -56,7 +57,7 @@ class ApplicationController < ActionController::API
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_writable_tournament!(tournament)
|
def require_writable_tournament!(tournament)
|
||||||
render_read_only_error if tournament.read_only_mode?
|
render_read_only_error if tournament&.read_only_mode?
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_not_found_error(exception)
|
def render_not_found_error(exception)
|
||||||
|
|
@ -67,4 +68,8 @@ class ApplicationController < ActionController::API
|
||||||
end
|
end
|
||||||
render json: { error: error }, status: :not_found
|
render json: { error: error }, status: :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_record_invalid_error(exception)
|
||||||
|
render json: exception.record.errors, status: :unprocessable_content
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class TeamsController < ApplicationController
|
class TeamsController < ApplicationController
|
||||||
before_action :set_team, only: %i[show update]
|
before_action :set_team, only: %i[show update]
|
||||||
before_action :authenticate_user!, only: %i[update]
|
before_action :authenticate_user!, only: %i[update]
|
||||||
before_action -> { require_owner! @team.owner }, only: %i[update]
|
before_action :require_team_update_actor!, only: %i[update]
|
||||||
before_action -> { require_writable_tournament!(@team.tournament) }, only: %i[update]
|
before_action -> { require_writable_tournament!(@team.tournament) }, only: %i[update]
|
||||||
|
|
||||||
# GET /teams/1
|
# GET /teams/1
|
||||||
|
|
@ -13,7 +13,11 @@ class TeamsController < ApplicationController
|
||||||
|
|
||||||
# PATCH/PUT /teams/1
|
# PATCH/PUT /teams/1
|
||||||
def update
|
def update
|
||||||
if @team.update(team_params)
|
@team.assign_attributes(team_params)
|
||||||
|
assign_player_from_params if player_assignment_requested?
|
||||||
|
return if performed?
|
||||||
|
|
||||||
|
if @team.save
|
||||||
push_sync_if_needed!(@team.tournament)
|
push_sync_if_needed!(@team.tournament)
|
||||||
render json: @team
|
render json: @team
|
||||||
else
|
else
|
||||||
|
|
@ -31,6 +35,49 @@ class TeamsController < ApplicationController
|
||||||
params.slice(:name).permit!
|
params.slice(:name).permit!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def player_assignment_requested?
|
||||||
|
params.key?(:player_email) || params.key?(:player_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_player_from_params
|
||||||
|
unless tournament_owner?
|
||||||
|
render_forbidden_error
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@team.player = if params.key?(:player_email)
|
||||||
|
player_from_email
|
||||||
|
else
|
||||||
|
player_from_id
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render json: e.record.errors, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def player_from_email
|
||||||
|
email = params[:player_email].to_s.strip
|
||||||
|
return nil if email.blank?
|
||||||
|
|
||||||
|
User.find_or_create_player_by_email!(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
def player_from_id
|
||||||
|
player_id = params[:player_id].to_s.strip
|
||||||
|
return nil if player_id.blank?
|
||||||
|
|
||||||
|
User.find(player_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_team_update_actor!
|
||||||
|
return if tournament_owner? || @team.player == current_user
|
||||||
|
|
||||||
|
render_forbidden_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def tournament_owner?
|
||||||
|
@team.owner == current_user
|
||||||
|
end
|
||||||
|
|
||||||
def push_sync_if_needed!(tournament)
|
def push_sync_if_needed!(tournament)
|
||||||
TournamentSyncEnqueue.call(tournament)
|
TournamentSyncEnqueue.call(tournament)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -236,10 +236,17 @@ class TournamentsController < ApplicationController
|
||||||
if team[:id]
|
if team[:id]
|
||||||
Team.find team[:id]
|
Team.find team[:id]
|
||||||
elsif team[:name]
|
elsif team[:name]
|
||||||
Team.create name: team[:name]
|
Team.create name: team[:name], player: player_from_team_params(team)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def player_from_team_params(team)
|
||||||
|
player_email = team[:player_email].to_s.strip
|
||||||
|
return nil if player_email.blank?
|
||||||
|
|
||||||
|
User.find_or_create_player_by_email!(player_email)
|
||||||
|
end
|
||||||
|
|
||||||
def set_tournament
|
def set_tournament
|
||||||
@tournament = Tournament.find(params[:id])
|
@tournament = Tournament.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
@ -249,7 +256,7 @@ class TournamentsController < ApplicationController
|
||||||
@tournament = profiling.measure('load_tournament') do
|
@tournament = profiling.measure('load_tournament') do
|
||||||
Tournament.includes(
|
Tournament.includes(
|
||||||
:user,
|
:user,
|
||||||
:teams,
|
{ teams: :player },
|
||||||
stages: [
|
stages: [
|
||||||
{ matches: { match_scores: :team } },
|
{ matches: { match_scores: :team } },
|
||||||
{ groups: [
|
{ groups: [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Team < ApplicationRecord
|
class Team < ApplicationRecord
|
||||||
belongs_to :tournament, optional: true
|
belongs_to :tournament, optional: true
|
||||||
|
belongs_to :player, class_name: 'User', optional: true, inverse_of: :player_teams
|
||||||
has_many :group_scores, dependent: :destroy
|
has_many :group_scores, dependent: :destroy
|
||||||
has_many :match_scores, dependent: :destroy
|
has_many :match_scores, dependent: :destroy
|
||||||
has_many :bets, dependent: :destroy
|
has_many :bets, dependent: :destroy
|
||||||
|
|
@ -9,5 +10,5 @@ class Team < ApplicationRecord
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament, allow_nil: true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,37 @@ class User < ApplicationRecord
|
||||||
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
validates :username, presence: true, uniqueness: { case_sensitive: false }
|
||||||
|
|
||||||
has_many :tournaments, dependent: :destroy
|
has_many :tournaments, dependent: :destroy
|
||||||
|
has_many :player_teams, class_name: 'Team', foreign_key: :player_id, inverse_of: :player, dependent: :nullify
|
||||||
|
has_many :played_tournaments, -> { distinct }, through: :player_teams, source: :tournament
|
||||||
has_many :bets, dependent: :destroy
|
has_many :bets, dependent: :destroy
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def find_or_create_player_by_email!(email)
|
||||||
|
normalized_email = email.to_s.strip.downcase
|
||||||
|
where('LOWER(email) = ?', normalized_email).first || create!(email: normalized_email) do |user|
|
||||||
|
generated_password = Devise.friendly_token.first(24)
|
||||||
|
user.username = unique_player_username(normalized_email)
|
||||||
|
user.password = generated_password
|
||||||
|
user.password_confirmation = generated_password
|
||||||
|
user.confirmed_at = Time.current
|
||||||
|
user.uid = normalized_email
|
||||||
|
user.provider = 'email'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def unique_player_username(email)
|
||||||
|
base = email.split('@').first.to_s.parameterize.presence || 'player'
|
||||||
|
candidate = base
|
||||||
|
suffix = 2
|
||||||
|
|
||||||
|
while where('LOWER(username) = ?', candidate.downcase).exists?
|
||||||
|
candidate = "#{base}-#{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
candidate
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountProfileSerializer < AccountSummarySerializer
|
||||||
|
attributes :tournaments, :created_tournaments, :teams
|
||||||
|
|
||||||
|
def tournaments
|
||||||
|
visible_played_tournaments.map { |tournament| tournament_summary(tournament) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_tournaments
|
||||||
|
visible_created_tournaments.map { |tournament| tournament_summary(tournament) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def teams
|
||||||
|
visible_player_teams.map do |team|
|
||||||
|
{
|
||||||
|
id: team.id,
|
||||||
|
name: team.name,
|
||||||
|
tournament: tournament_summary(team.tournament)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def visible_played_tournaments
|
||||||
|
visible_tournaments(object.played_tournaments.order(:created_at))
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible_created_tournaments
|
||||||
|
visible_tournaments(object.tournaments.order(:created_at))
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible_player_teams
|
||||||
|
object.player_teams.includes(:tournament).order(:created_at).select do |team|
|
||||||
|
profile_owner? || team.tournament&.public?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible_tournaments(scope)
|
||||||
|
return scope if profile_owner?
|
||||||
|
|
||||||
|
scope.where(public: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile_owner?
|
||||||
|
scope == object
|
||||||
|
end
|
||||||
|
|
||||||
|
def tournament_summary(tournament)
|
||||||
|
return nil if tournament.nil?
|
||||||
|
|
||||||
|
{
|
||||||
|
id: tournament.id,
|
||||||
|
name: tournament.name,
|
||||||
|
code: tournament.code,
|
||||||
|
public: tournament.public
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountSummarySerializer < ApplicationSerializer
|
||||||
|
attributes :name, :username
|
||||||
|
|
||||||
|
def name
|
||||||
|
object.username
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TeamSerializer < ApplicationSerializer
|
class TeamSerializer < ApplicationSerializer
|
||||||
attributes :name
|
attributes :name, :player
|
||||||
|
|
||||||
|
def player
|
||||||
|
AccountSummarySerializer.new(object.player).as_json if object.player
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class TournamentSerializer < SimpleTournamentSerializer
|
||||||
{
|
{
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
|
player: team.player && AccountSummarySerializer.new(team.player).as_json,
|
||||||
advancing_from_group_stage: adv_teams.include?(team)
|
advancing_from_group_stage: adv_teams.include?(team)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class TournamentSyncSchema
|
||||||
}.freeze,
|
}.freeze,
|
||||||
Team => {
|
Team => {
|
||||||
synced: %w[id name].freeze,
|
synced: %w[id name].freeze,
|
||||||
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
|
ignored: %w[tournament_id player_id created_at updated_at sync_source_id].freeze
|
||||||
}.freeze,
|
}.freeze,
|
||||||
Beamer => {
|
Beamer => {
|
||||||
synced: %w[id name display_state is_default config].freeze,
|
synced: %w[id name display_state is_default config].freeze,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
resources :stages, only: %i[show update]
|
resources :stages, only: %i[show update]
|
||||||
resources :teams, only: %i[show update]
|
resources :teams, only: %i[show update]
|
||||||
|
resources :accounts, only: %i[show]
|
||||||
resources :team_action_items, only: %i[update]
|
resources :team_action_items, only: %i[update]
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
resources :team_action_lists, only: %i[create]
|
resources :team_action_lists, only: %i[create]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddPlayerToTeams < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_reference :teams, :player, type: :integer, foreign_key: { to_table: :users }, index: true
|
||||||
|
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_120000) 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
|
||||||
|
|
@ -124,9 +124,11 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
||||||
create_table "teams", force: :cascade do |t|
|
create_table "teams", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.string "name"
|
t.string "name"
|
||||||
|
t.integer "player_id"
|
||||||
t.integer "sync_source_id"
|
t.integer "sync_source_id"
|
||||||
t.integer "tournament_id"
|
t.integer "tournament_id"
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
|
t.index ["player_id"], name: "index_teams_on_player_id"
|
||||||
t.index ["tournament_id"], name: "index_teams_on_tournament_id"
|
t.index ["tournament_id"], name: "index_teams_on_tournament_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -234,6 +236,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_30_121000) do
|
||||||
add_foreign_key "team_action_items", "teams", on_delete: :cascade
|
add_foreign_key "team_action_items", "teams", on_delete: :cascade
|
||||||
add_foreign_key "team_action_lists", "tournaments", on_delete: :cascade
|
add_foreign_key "team_action_lists", "tournaments", on_delete: :cascade
|
||||||
add_foreign_key "teams", "tournaments", on_delete: :cascade
|
add_foreign_key "teams", "tournaments", on_delete: :cascade
|
||||||
|
add_foreign_key "teams", "users", column: "player_id"
|
||||||
add_foreign_key "tournament_sync_queue_entries", "tournaments", on_delete: :cascade
|
add_foreign_key "tournament_sync_queue_entries", "tournaments", on_delete: :cascade
|
||||||
add_foreign_key "tournament_transaction_log_entries", "tournaments", on_delete: :cascade
|
add_foreign_key "tournament_transaction_log_entries", "tournaments", on_delete: :cascade
|
||||||
add_foreign_key "tournament_transaction_log_entries", "users", on_delete: :nullify
|
add_foreign_key "tournament_transaction_log_entries", "users", on_delete: :nullify
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe AccountsController, type: :controller do
|
||||||
|
describe 'GET #show' do
|
||||||
|
let(:account) { create(:user, username: 'player-one') }
|
||||||
|
let(:public_team) { create(:team, name: 'Public Team', player: account) }
|
||||||
|
let(:private_team) { create(:team, name: 'Private Team', player: account) }
|
||||||
|
let(:public_tournament) { create(:tournament, public: true, teams: [public_team]) }
|
||||||
|
let(:private_tournament) { create(:tournament, public: false, teams: [private_team]) }
|
||||||
|
let(:created_public_tournament) { create(:tournament, user: account, public: true) }
|
||||||
|
let(:created_private_tournament) { create(:tournament, user: account, public: false) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
public_tournament
|
||||||
|
private_tournament
|
||||||
|
created_public_tournament
|
||||||
|
created_private_tournament
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns public account profile information' do
|
||||||
|
get :show, params: { id: account.to_param }
|
||||||
|
|
||||||
|
body = deserialize_response(response)
|
||||||
|
expect(response).to be_successful
|
||||||
|
expect(body[:id]).to eq(account.id)
|
||||||
|
expect(body[:name]).to eq('player-one')
|
||||||
|
expect(body[:username]).to eq('player-one')
|
||||||
|
expect(body[:teams].map { |team| team[:id] }).to eq([public_team.id])
|
||||||
|
expect(body[:tournaments].map { |tournament| tournament[:id] }).to eq([public_tournament.id])
|
||||||
|
expect(body[:created_tournaments].map { |tournament| tournament[:id] }).to eq([created_public_tournament.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns private profile information to the account owner' do
|
||||||
|
apply_authentication_headers_for account
|
||||||
|
|
||||||
|
get :show, params: { id: account.to_param }
|
||||||
|
|
||||||
|
body = deserialize_response(response)
|
||||||
|
expect(body[:teams].map { |team| team[:id] }).to match_array([public_team.id, private_team.id])
|
||||||
|
expect(body[:tournaments].map { |tournament| tournament[:id] })
|
||||||
|
.to match_array([public_tournament.id, private_tournament.id])
|
||||||
|
expect(body[:created_tournaments].map { |tournament| tournament[:id] })
|
||||||
|
.to match_array([created_public_tournament.id, created_private_tournament.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns not found for unknown accounts' do
|
||||||
|
get :show, params: { id: User.maximum(:id).to_i + 1 }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -19,6 +19,17 @@ RSpec.describe TeamsController, type: :controller do
|
||||||
body = deserialize_response response
|
body = deserialize_response response
|
||||||
expect(body[:name]).to eq(@team.name)
|
expect(body[:name]).to eq(@team.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns player account summary when linked' do
|
||||||
|
player = create(:user, username: 'linked-player')
|
||||||
|
@team.update!(player: player)
|
||||||
|
|
||||||
|
get :show, params: { id: @team.to_param }
|
||||||
|
|
||||||
|
body = deserialize_response response
|
||||||
|
expect(body.dig(:player, :id)).to eq(player.id)
|
||||||
|
expect(body.dig(:player, :name)).to eq('linked-player')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
|
|
@ -46,6 +57,35 @@ RSpec.describe TeamsController, type: :controller do
|
||||||
body = deserialize_response response
|
body = deserialize_response response
|
||||||
expect(body[:name]).to eq(valid_update[:name])
|
expect(body[:name]).to eq(valid_update[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'associates an existing player account by email' do
|
||||||
|
player = create(:user, username: 'player-account', email: 'player@example.com')
|
||||||
|
|
||||||
|
put :update, params: { id: @team.to_param, player_email: player.email }
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
expect(@team.reload.player).to eq(player)
|
||||||
|
expect(deserialize_response(response).dig(:player, :id)).to eq(player.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a player account by email when none exists' do
|
||||||
|
expect do
|
||||||
|
put :update, params: { id: @team.to_param, player_email: 'new-player@example.com' }
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
expect(@team.reload.player.email).to eq('new-player@example.com')
|
||||||
|
expect(@team.player.username).to eq('new-player')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears player association with blank player_id' do
|
||||||
|
@team.update!(player: create(:user))
|
||||||
|
|
||||||
|
put :update, params: { id: @team.to_param, player_id: '' }
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
expect(@team.reload.player).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with valid params as another user' do
|
context 'with valid params as another user' do
|
||||||
|
|
@ -58,5 +98,29 @@ RSpec.describe TeamsController, type: :controller do
|
||||||
expect(response).to have_http_status(:forbidden)
|
expect(response).to have_http_status(:forbidden)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with valid params as linked player' do
|
||||||
|
before(:each) do
|
||||||
|
@player = create(:user)
|
||||||
|
@team.update!(player: @player)
|
||||||
|
apply_authentication_headers_for @player
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the requested team name' do
|
||||||
|
put :update, params: { id: @team.to_param }.merge(valid_update)
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
expect(@team.reload.name).to eq(valid_update[:name])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cannot reassign the linked player' do
|
||||||
|
new_player = create(:user, email: 'other-player@example.com')
|
||||||
|
|
||||||
|
put :update, params: { id: @team.to_param, player_email: new_player.email }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(@team.reload.player).to eq(@player)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,25 @@ RSpec.describe TournamentsController, type: :controller do
|
||||||
post :create, params: data
|
post :create, params: data
|
||||||
end.to change(Team, :count).by(data[:teams].count)
|
end.to change(Team, :count).by(data[:teams].count)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates player accounts from team email values' do
|
||||||
|
data = create_playoff_tournament_data
|
||||||
|
data.delete :teams
|
||||||
|
data[:teams] = [
|
||||||
|
{ name: 'Alpha', player_email: 'alpha-player@example.com' },
|
||||||
|
{ name: 'Beta' },
|
||||||
|
{ name: 'Gamma' },
|
||||||
|
{ name: 'Delta' }
|
||||||
|
]
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post :create, params: data
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
tournament = Tournament.find(deserialize_response(response)[:id])
|
||||||
|
expect(tournament.teams.find_by(name: 'Alpha').player.email).to eq('alpha-player@example.com')
|
||||||
|
expect(tournament.teams.find_by(name: 'Beta').player).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with invalid parameters' do
|
context 'with invalid parameters' do
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,61 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(unauthenticated[:status]).to eq(401)
|
expect(unauthenticated[:status]).to eq(401)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'links player accounts to teams and exposes public account profiles' do
|
||||||
|
unless ENV['TURNIERE_E2E_ALT_EMAIL'] && ENV['TURNIERE_E2E_ALT_USERNAME']
|
||||||
|
skip('player profile flow requires TURNIERE_E2E_ALT_EMAIL and TURNIERE_E2E_ALT_USERNAME')
|
||||||
|
end
|
||||||
|
|
||||||
|
unique = unique_suffix
|
||||||
|
player_email = ENV.fetch('TURNIERE_E2E_ALT_EMAIL')
|
||||||
|
player_username = ENV.fetch('TURNIERE_E2E_ALT_USERNAME')
|
||||||
|
player_client = other_client
|
||||||
|
player_created_tournament = create_playoff_tournament(
|
||||||
|
client: player_client,
|
||||||
|
public: true,
|
||||||
|
name_prefix: 'Player Created'
|
||||||
|
)
|
||||||
|
tournament = create_playoff_tournament(client: owner_client, public: true, name_prefix: 'Player Profile')
|
||||||
|
linked_team = tournament.fetch(:teams).first
|
||||||
|
unlinked_team = tournament.fetch(:teams).fetch(1)
|
||||||
|
|
||||||
|
link_existing = owner_client.patch("/teams/#{linked_team.fetch(:id)}", body: { player_email: player_email })
|
||||||
|
player_update = player_client.patch("/teams/#{linked_team.fetch(:id)}", body: {
|
||||||
|
name: "#{linked_team.fetch(:name)} Player Rename"
|
||||||
|
})
|
||||||
|
player_reassign = player_client.patch("/teams/#{linked_team.fetch(:id)}", body: {
|
||||||
|
player_email: "stolen-#{unique}@example.com"
|
||||||
|
})
|
||||||
|
placeholder_email = "http-e2e-placeholder-#{unique}@example.com"
|
||||||
|
link_placeholder = owner_client.patch("/teams/#{unlinked_team.fetch(:id)}", body: {
|
||||||
|
player_email: placeholder_email
|
||||||
|
})
|
||||||
|
profile = anonymous_client.get("/accounts/#{link_existing.dig(:json, :player, :id)}")
|
||||||
|
placeholder_profile = anonymous_client.get("/accounts/#{link_placeholder.dig(:json, :player, :id)}")
|
||||||
|
|
||||||
|
expect(player_client.authenticated?).to eq(true)
|
||||||
|
expect(linked_team[:player]).to be_nil
|
||||||
|
|
||||||
|
expect(link_existing[:status]).to eq(200)
|
||||||
|
expect(link_existing.dig(:json, :player, :name)).to eq(player_username)
|
||||||
|
|
||||||
|
expect(player_update[:status]).to eq(200)
|
||||||
|
expect(player_update.dig(:json, :name)).to end_with('Player Rename')
|
||||||
|
expect(player_reassign[:status]).to eq(403)
|
||||||
|
|
||||||
|
expect(link_placeholder[:status]).to eq(200)
|
||||||
|
expect(link_placeholder.dig(:json, :player, :name)).to start_with('http-e2e-placeholder')
|
||||||
|
expect(placeholder_profile[:status]).to eq(200)
|
||||||
|
expect(placeholder_profile.dig(:json, :teams).map { |team| team.fetch(:id) }).to include(unlinked_team.fetch(:id))
|
||||||
|
|
||||||
|
expect(profile[:status]).to eq(200)
|
||||||
|
expect(profile.dig(:json, :name)).to eq(player_username)
|
||||||
|
expect(profile.dig(:json, :teams).map { |team| team.fetch(:id) }).to include(linked_team.fetch(:id))
|
||||||
|
expect(profile.dig(:json, :tournaments).map { |item| item.fetch(:id) }).to include(tournament.fetch(:id))
|
||||||
|
expect(profile.dig(:json, :created_tournaments).map { |item| item.fetch(:id) })
|
||||||
|
.to include(player_created_tournament.fetch(:id))
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates and destroys tournaments, enforces owner checks, and validates timer updates' do
|
it 'updates and destroys tournaments, enforces owner checks, and validates timer updates' do
|
||||||
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Mutable Tournament')
|
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Mutable Tournament')
|
||||||
|
|
||||||
|
|
@ -562,12 +617,12 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
||||||
expect(response[:json].map { |match| match.fetch(:state) }.uniq).to eq(['in_progress'])
|
expect(response[:json].map { |match| match.fetch(:state) }.uniq).to eq(['in_progress'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_playoff_tournament(client:, public:, name_prefix:)
|
def create_playoff_tournament(client:, public:, name_prefix:, teams: nil)
|
||||||
payload = {
|
payload = {
|
||||||
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,
|
||||||
teams: 4.times.map do |index|
|
teams: teams || 4.times.map do |index|
|
||||||
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
{ name: "#{name_prefix.tr(' ', '')}-S#{index + 1}" }
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ RSpec.describe Team, type: :model do
|
||||||
|
|
||||||
describe 'association' do
|
describe 'association' do
|
||||||
it { should belong_to(:tournament).optional }
|
it { should belong_to(:tournament).optional }
|
||||||
|
it { should belong_to(:player).optional }
|
||||||
it { should have_many :group_scores }
|
it { should have_many :group_scores }
|
||||||
it { should have_many :match_scores }
|
it { should have_many :match_scores }
|
||||||
it { should have_many :bets }
|
it { should have_many :bets }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ require 'rails_helper'
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
describe 'association' do
|
describe 'association' do
|
||||||
it { should have_many :tournaments }
|
it { should have_many :tournaments }
|
||||||
|
it { should have_many :player_teams }
|
||||||
|
it { should have_many :played_tournaments }
|
||||||
it { should have_many :bets }
|
it { should have_many :bets }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe AccountsController, type: :routing do
|
||||||
|
describe 'routing' do
|
||||||
|
it 'routes to #show' do
|
||||||
|
expect(get: '/accounts/1').to route_to('accounts#show', id: '1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue