diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index ac40b8f..8a83897 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -45,12 +45,8 @@ class MatchesController < ApplicationController Match.transaction do if @match.update(match_params) handle_match_end if new_state == 'finished' - if @match.group_match? && new_state == 'in_progress' - group = @match.group - unless UpdateGroupsGroupScoresAndSave.call(group: group).success? - logger.warn "Updating groups group score failed for #{group}" - end - end + handle_match_stop if new_state == 'not_started' + update_group_scores if @match.group_match? && %w[in_progress not_started].include?(new_state) render json: @match synced_tournament = @match.tournament @@ -113,6 +109,19 @@ class MatchesController < ApplicationController raise ActiveRecord::Rollback end + def handle_match_stop + @match.match_scores.each do |match_score| + match_score.update!(points: 0, hidden_points: 0) + end + end + + def update_group_scores + group = @match.group + return if UpdateGroupsGroupScoresAndSave.call(group: group).success? + + logger.warn "Updating groups group score failed for #{group}" + end + def validate_params case match_params['state'] when 'in_progress' @@ -121,6 +130,9 @@ class MatchesController < ApplicationController when 'finished' render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_content \ unless @match.in_progress? + when 'not_started' + render json: { error: 'Match can\'t stop in this state' }, status: :unprocessable_content \ + unless @match.in_progress? else render json: { error: 'Invalid target state' }, status: :unprocessable_content end diff --git a/spec/controllers/matches_controller_spec.rb b/spec/controllers/matches_controller_spec.rb index 96253c2..ed1a5a0 100644 --- a/spec/controllers/matches_controller_spec.rb +++ b/spec/controllers/matches_controller_spec.rb @@ -233,6 +233,21 @@ RSpec.describe MatchesController, type: :controller do expect(body[:state]).to eq(valid_update[:state]) end + it 'stops an in-progress match and resets its scores' do + @match.update!(state: :in_progress) + @match.match_scores.each do |match_score| + match_score.update!(points: 7, hidden_points: 5) + end + + expect(TournamentLiveBroadcast).to receive(:call).with(@match.tournament.id) + put :update, params: { id: @match.to_param, state: 'not_started' } + + expect(response).to be_successful + expect(@match.reload.state).to eq('not_started') + expect(@match.match_scores.reload.map(&:points)).to all(eq(0)) + expect(@match.match_scores.reload.map(&:hidden_points)).to all(eq(0)) + end + context 'on a running playoff match' do let(:finished) do { @@ -385,6 +400,14 @@ RSpec.describe MatchesController, type: :controller do expect(response).to have_http_status(:unprocessable_content) end end + + context 'with stop params on a not-started match' do + it 'renders an unprocessable entity response' do + put :update, params: { id: @match.to_param, state: 'not_started' } + expect(response).to have_http_status(:unprocessable_content) + expect(deserialize_response(response)[:error]).to eq("Match can't stop in this state") + end + end end context 'as another user' do @@ -401,6 +424,34 @@ RSpec.describe MatchesController, type: :controller do end end + context 'on a running group match' do + let(:tournament) { create(:group_stage_tournament, group_count: 1, match_factory: :filled_group_match) } + let(:group) { tournament.stages.find_by(level: -1).groups.first } + let(:group_match) { group.matches.first } + + before do + apply_authentication_headers_for tournament.owner + group_match.update!(state: :in_progress) + group_match.match_scores.order(:id).each_with_index do |match_score, index| + match_score.update!(points: index.zero? ? 9 : 2, hidden_points: index.zero? ? 4 : 1) + end + UpdateGroupsGroupScoresAndSave.call(group: group) + end + + it 'stops the match, clears scores, and recalculates group scores' do + put :update, params: { id: group_match.to_param, state: 'not_started' } + + expect(response).to be_successful + expect(group_match.reload.state).to eq('not_started') + expect(group_match.match_scores.reload.map(&:points)).to all(eq(0)) + expect(group_match.match_scores.reload.map(&:hidden_points)).to all(eq(0)) + involved_group_scores = group.group_scores.reload.select { |score| group_match.teams.include?(score.team) } + expect(involved_group_scores.map(&:group_points)).to all(eq(0)) + expect(involved_group_scores.map(&:scored_points)).to all(eq(0)) + expect(involved_group_scores.map(&:received_points)).to all(eq(0)) + end + end + context 'on a playoff match that isn\'t ready yet' do let(:invalid_update) do { diff --git a/spec/e2e/http/api_surface_spec.rb b/spec/e2e/http/api_surface_spec.rb index b63963c..d72a158 100644 --- a/spec/e2e/http/api_surface_spec.rb +++ b/spec/e2e/http/api_surface_spec.rb @@ -270,11 +270,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do started_match = anonymous_client.get("/matches/#{match.fetch(:id)}") score_to_update = started_match.fetch(:json).fetch(:match_scores).first score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}") - score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 }) + score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7, hidden_points: 11 }) forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 }) bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) }) restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' }) + stop_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'not_started' }) + stopped_match = anonymous_client.get("/matches/#{match.fetch(:id)}") + restop_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'not_started' }) playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics") tied_playoff_match = playoff_only.fetch(:stages) .flat_map { |stage| stage.fetch(:matches) } @@ -349,12 +352,21 @@ RSpec.describe 'Backend API surface HTTP E2E' do expect(score_show[:status]).to eq(200) expect(score_update[:status]).to eq(200) expect(score_update.dig(:json, :points)).to eq(7) + expect(score_update.dig(:json, :hidden_points)).to eq(11) expect(forbidden_score_update[:status]).to eq(403) expect(unauthenticated_score_update[:status]).to eq(401) expect(bet_while_running[:status]).to eq(422) expect(bet_while_running.dig(:json, :error)).to eq('Betting is not allowed while match is in_progress') expect(restart_started_match[:status]).to eq(422) expect(restart_started_match.dig(:json, :error)).to eq("Match can't start in this state") + expect(stop_started_match[:status]).to eq(200) + expect(stop_started_match.dig(:json, :state)).to eq('not_started') + expect(stopped_match[:status]).to eq(200) + expect(stopped_match.dig(:json, :state)).to eq('not_started') + expect(stopped_match.dig(:json, :match_scores).map { |score| score.fetch(:points) }).to all(eq(0)) + expect(stopped_match.dig(:json, :match_scores).map { |score| score.fetch(:hidden_points) }).to all(eq(0)) + expect(restop_match[:status]).to eq(422) + expect(restop_match.dig(:json, :error)).to eq("Match can't stop in this state") expect(playoff_stats[:status]).to eq(501) expect(playoff_stats[:json]).to eq({})