Add backend group match swap endpoint
This commit is contained in:
parent
9547cce982
commit
99538a3343
|
|
@ -1,13 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MatchesController < ApplicationController
|
||||
before_action :set_match, only: %i[show update]
|
||||
before_action :set_match, only: %i[show update swap]
|
||||
before_action :set_group, only: %i[create]
|
||||
before_action :validate_params, only: %i[update]
|
||||
before_action -> { require_owner! @match.owner }, only: %i[update]
|
||||
before_action :set_swap_target, only: %i[swap]
|
||||
before_action -> { require_owner! @match.owner }, only: %i[update swap]
|
||||
before_action -> { require_owner! @group.owner }, only: %i[create]
|
||||
before_action :set_tournament, only: %i[index]
|
||||
before_action -> { require_writable_tournament!(@match.tournament) }, only: %i[update]
|
||||
before_action -> { require_writable_tournament!(@match.tournament) }, only: %i[update swap]
|
||||
before_action -> { require_writable_tournament!(@group.tournament) }, only: %i[create]
|
||||
|
||||
# GET/tournaments/1/matches
|
||||
|
|
@ -88,6 +89,30 @@ class MatchesController < ApplicationController
|
|||
push_sync_if_needed!(synced_tournament) unless synced_tournament.nil?
|
||||
end
|
||||
|
||||
# PATCH /matches/1/swap
|
||||
def swap
|
||||
unless @match.swappable_with?(@swap_target)
|
||||
render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
synced_tournament = nil
|
||||
|
||||
Match.transaction do
|
||||
source_position = @match.position
|
||||
target_position = @swap_target.position
|
||||
|
||||
@match.update!(position: target_position)
|
||||
@swap_target.update!(position: source_position)
|
||||
|
||||
synced_tournament = @match.tournament
|
||||
end
|
||||
|
||||
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
|
||||
render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group]
|
||||
push_sync_if_needed!(synced_tournament)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_match_end
|
||||
|
|
@ -134,6 +159,13 @@ class MatchesController < ApplicationController
|
|||
@tournament = Tournament.find(params[:tournament_id])
|
||||
end
|
||||
|
||||
def set_swap_target
|
||||
@swap_target = Match.find_by(id: params[:other_match_id])
|
||||
return unless @swap_target.nil?
|
||||
|
||||
render json: { error: 'Match not found' }, status: :not_found
|
||||
end
|
||||
|
||||
def set_group
|
||||
@group = Group.find(params[:group_id])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ class Match < ApplicationRecord
|
|||
group.present?
|
||||
end
|
||||
|
||||
def swappable_with?(other_match)
|
||||
return false if other_match.nil?
|
||||
|
||||
group_match? && other_match.group_match? && group_id == other_match.group_id && id != other_match.id
|
||||
end
|
||||
|
||||
def scored_points_of(team)
|
||||
return 0 if decider?
|
||||
return 0 unless (finished? || in_progress?) && teams.include?(team)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ Rails.application.routes.draw do
|
|||
}
|
||||
|
||||
resources :matches, only: %i[show update] do
|
||||
member do
|
||||
patch :swap
|
||||
end
|
||||
resources :bets, only: %i[index create]
|
||||
end
|
||||
resources :groups, only: %i[show] do
|
||||
|
|
|
|||
|
|
@ -96,6 +96,72 @@ RSpec.describe MatchesController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PATCH #swap' do
|
||||
let(:group_stage_tournament) { create(:group_stage_tournament, group_count: 2, match_factory: :filled_group_match) }
|
||||
let(:group_stage) { group_stage_tournament.stages.find_by(level: -1) }
|
||||
let(:first_group) { group_stage.groups.order(:id).first }
|
||||
let(:second_group) { group_stage.groups.order(:id).second }
|
||||
let(:first_match) do
|
||||
first_group.matches.order(:id).first.tap do |match|
|
||||
match.update!(position: 0)
|
||||
end
|
||||
end
|
||||
let(:last_match) do
|
||||
first_group.matches.order(:id).last.tap do |match|
|
||||
match.update!(position: 3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as owner' do
|
||||
before do
|
||||
apply_authentication_headers_for group_stage_tournament.owner
|
||||
end
|
||||
|
||||
it 'swaps positions of two matches from same group' do
|
||||
original_first_position = first_match.position
|
||||
original_last_position = last_match.position
|
||||
|
||||
patch :swap, params: { id: first_match.to_param, other_match_id: last_match.to_param }
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(first_match.reload.position).to eq(original_last_position)
|
||||
expect(last_match.reload.position).to eq(original_first_position)
|
||||
|
||||
body = deserialize_response(response)
|
||||
expect(body.map { |match| match[:id] }).to match_array([first_match.id, last_match.id])
|
||||
expect(body.map { |match| match[:position] }).to match_array([original_first_position, original_last_position])
|
||||
end
|
||||
|
||||
it 'rejects matches from different groups' do
|
||||
foreign_match = second_group.matches.order(:id).first
|
||||
|
||||
patch :swap, params: { id: first_match.to_param, other_match_id: foreign_match.to_param }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(deserialize_response(response)[:error]).to eq('Matches must be different group matches from the same group')
|
||||
end
|
||||
|
||||
it 'rejects missing swap target' do
|
||||
patch :swap, params: { id: first_match.to_param, other_match_id: Match.maximum(:id).to_i + 1 }
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(deserialize_response(response)[:error]).to eq('Match not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'as another user' do
|
||||
before do
|
||||
apply_authentication_headers_for create(:user)
|
||||
end
|
||||
|
||||
it 'renders a forbidden error response' do
|
||||
patch :swap, params: { id: first_match.to_param, other_match_id: last_match.to_param }
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #update' do
|
||||
context 'on a running playoff match' do
|
||||
let(:valid_update) do
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
require 'securerandom'
|
||||
require 'time'
|
||||
require_relative '../../../e2e/lib/api_client'
|
||||
|
||||
RSpec.describe 'Backend API surface HTTP E2E' do
|
||||
|
|
@ -300,6 +301,47 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
expect(statistics_after.dig(:json, :least_dominant_score, :id)).to be_a(Integer)
|
||||
end
|
||||
|
||||
it 'swaps group match positions and keeps upcoming order consistent afterwards' do
|
||||
tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Swap Matches')
|
||||
group_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
group = group_stage.fetch(:groups).min_by { |item| item.fetch(:number) }
|
||||
ordered_matches = group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
||||
first_match = ordered_matches.fetch(0)
|
||||
second_match = ordered_matches.fetch(1)
|
||||
fourth_match = ordered_matches.fetch(3)
|
||||
|
||||
upcoming_before = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
swap_response = owner_client.patch("/matches/#{first_match.fetch(:id)}/swap", body: {
|
||||
other_match_id: fourth_match.fetch(:id)
|
||||
})
|
||||
swapped_tournament = fetch_tournament(client: owner_client, tournament_id: tournament.fetch(:id))
|
||||
swapped_group = swapped_tournament.fetch(:stages)
|
||||
.find { |stage| stage.fetch(:level) == -1 }
|
||||
.fetch(:groups)
|
||||
.find { |item| item.fetch(:id) == group.fetch(:id) }
|
||||
swapped_matches = swapped_group.fetch(:matches).sort_by { |item| item.fetch(:position) }
|
||||
upcoming_after_swap = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
|
||||
finish_group_match_with_winner!(match_id: fourth_match.fetch(:id))
|
||||
|
||||
upcoming_after_finish = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' })
|
||||
|
||||
expect(upcoming_before[:status]).to eq(200)
|
||||
expect(upcoming_match_for_group(upcoming_before, group_id: group.fetch(:id)).fetch(:id)).to eq(first_match.fetch(:id))
|
||||
|
||||
expect(swap_response[:status]).to eq(200)
|
||||
expect(swap_response[:json].map { |match| match[:id] }).to match_array([first_match.fetch(:id), fourth_match.fetch(:id)])
|
||||
|
||||
expect(swapped_matches.first.fetch(:id)).to eq(fourth_match.fetch(:id))
|
||||
expect(swapped_matches.first.fetch(:position)).to eq(first_match.fetch(:position))
|
||||
expect(swapped_matches.find { |match| match.fetch(:id) == first_match.fetch(:id) }.fetch(:position)).to eq(fourth_match.fetch(:position))
|
||||
expect(upcoming_after_swap[:status]).to eq(200)
|
||||
expect(upcoming_match_for_group(upcoming_after_swap, group_id: group.fetch(:id)).fetch(:id)).to eq(fourth_match.fetch(:id))
|
||||
|
||||
expect(upcoming_after_finish[:status]).to eq(200)
|
||||
expect(upcoming_match_for_group(upcoming_after_finish, group_id: group.fetch(:id)).fetch(:id)).to eq(second_match.fetch(:id))
|
||||
end
|
||||
|
||||
def login_client(email:, password:)
|
||||
client = TurniereE2E::ApiClient.new(base_url: base_url)
|
||||
response = client.login!(email: email, password: password)
|
||||
|
|
@ -401,6 +443,10 @@ RSpec.describe 'Backend API surface HTTP E2E' do
|
|||
end
|
||||
end
|
||||
|
||||
def upcoming_match_for_group(response, group_id:)
|
||||
response.fetch(:json).find { |match| match.dig(:group, :id) == group_id }
|
||||
end
|
||||
|
||||
def unique_suffix
|
||||
@unique_suffix_counter ||= 0
|
||||
@unique_suffix_counter += 1
|
||||
|
|
|
|||
|
|
@ -11,5 +11,9 @@ RSpec.describe MatchesController, type: :routing do
|
|||
it 'routes nested group matches to #create' do
|
||||
expect(post: '/groups/1/matches').to route_to('matches#create', group_id: '1')
|
||||
end
|
||||
|
||||
it 'routes to #swap' do
|
||||
expect(patch: '/matches/1/swap').to route_to('matches#swap', id: '1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue