diff --git a/Gemfile.lock b/Gemfile.lock index d40cadd..0779004 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -328,6 +328,7 @@ PLATFORMS aarch64-linux-musl arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index a40c6bf..2e33843 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true class TournamentsController < ApplicationController - before_action :set_tournament, only: %i[show update destroy] - before_action :authenticate_user!, only: %i[create update destroy] - before_action -> { require_owner! @tournament.owner }, only: %i[update destroy] + before_action :set_tournament, only: %i[show update destroy set_timer_end timer_end] + before_action :authenticate_user!, only: %i[create update destroy set_timer_end] + before_action -> { require_owner! @tournament.owner }, only: %i[update destroy set_timer_end] before_action :validate_create_params, only: %i[create] before_action :validate_update_params, only: %i[update] + before_action :validate_set_timer_end_params, only: %i[set_timer_end] rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error # GET /tournaments @@ -93,8 +94,27 @@ class TournamentsController < ApplicationController @tournament.destroy end + # GET /tournaments/:id/timer_end + def timer_end + render json: { timer_end: @tournament.timer_end } + end + + # PATCH /tournaments/:id/set_timer_end + def set_timer_end + if @tournament.update(timer_end_params) + render json: @tournament + else + render json: @tournament.errors, status: :unprocessable_entity + end + end + + private + def timer_end_params + { timer_end: params[:timer_end] } + end + def organize_teams_in_groups(teams) # each team gets put into a array of teams depending on the group specified in team[:group] teams.group_by { |team| team['group'] }.values.map do |group| @@ -158,3 +178,39 @@ class TournamentsController < ApplicationController }, status: :unprocessable_entity end end + +def validate_set_timer_end_params + timer_end = params[:timer_end] + timer_end_seconds = params[:timer_end_seconds] + + # throw error if both timer_end and timer_end_seconds are present + if timer_end.present? && timer_end_seconds.present? + return render json: { error: 'Only one of timer_end or timer_end_seconds is allowed' }, status: :unprocessable_entity + end + + if timer_end_seconds.present? + begin + timer_end_seconds = Integer(timer_end_seconds) + rescue ArgumentError + return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity + end + + return render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity if timer_end_seconds <= 0 + + parsed_time = Time.zone.now + timer_end_seconds + params[:timer_end] = parsed_time + elsif timer_end.present? + begin + parsed_time = Time.zone.parse(timer_end) + if parsed_time.nil? + return render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity + elsif !parsed_time.future? + return render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity + end + rescue ArgumentError + return render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity + end + else + return render json: { error: 'Timer end is required' }, status: :unprocessable_entity + end +end diff --git a/app/serializers/tournament_serializer.rb b/app/serializers/tournament_serializer.rb index 25582e1..979a19a 100644 --- a/app/serializers/tournament_serializer.rb +++ b/app/serializers/tournament_serializer.rb @@ -2,7 +2,7 @@ class TournamentSerializer < SimpleTournamentSerializer attributes :description, :playoff_teams_amount, - :instant_finalists_amount, :intermediate_round_participants_amount + :instant_finalists_amount, :intermediate_round_participants_amount, :timer_end has_many :stages has_many :teams diff --git a/config/routes.rb b/config/routes.rb index 101e869..9a6cf02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,10 @@ Rails.application.routes.draw do resources :tournaments do resources :statistics, only: %i[index] resources :matches, only: %i[index] + member do + get :timer_end + patch :set_timer_end + end end resources :match_scores, only: %i[show update] resources :groups, only: %i[show] diff --git a/db/migrate/20250309202658_add_timer_end_to_tournaments.rb b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb new file mode 100644 index 0000000..1762bec --- /dev/null +++ b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb @@ -0,0 +1,5 @@ +class AddTimerEndToTournaments < ActiveRecord::Migration[7.0] + def change + add_column :tournaments, :timer_end, :datetime + end +end diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index ae8f469..f6574ee 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -100,7 +100,11 @@ RSpec.describe TournamentsController, type: :controller do it 'returns the requested tournament' do get :show, params: { id: @tournament.to_param } - expect(deserialize_response(response)[:id].to_i).to eq(@tournament.id) + json = deserialize_response(response) + expect(json[:id].to_i).to eq(@tournament.id) + expected_keys = %i[id name code public description playoff_teams_amount instant_finalists_amount intermediate_round_participants_amount timer_end owner_username stages teams] + expect(json.keys).to match_array(expected_keys) + expect(json).to eq(TournamentSerializer.new(@tournament).as_json) end context 'with simple=true parameter' do @@ -477,4 +481,96 @@ RSpec.describe TournamentsController, type: :controller do end end end + + describe 'PATCH #set_timer_end' do + before(:each) do + apply_authentication_headers_for @tournament.owner + @request.env['HTTP_ACCEPT'] = 'application/json' + end + context 'timer_end' do + context 'when timer_end is missing' do + it 'returns unprocessable entity' do + patch :set_timer_end, params: { id: @tournament.id } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Timer end is required') + end + end + + context 'when timer_end is invalid datetime' do + it 'returns unprocessable entity' do + patch :set_timer_end, params: { id: @tournament.id, timer_end: 'invalid' } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Invalid datetime format') + end + end + + context 'when timer_end is in the past' do + it 'returns unprocessable entity' do + patch :set_timer_end, params: { id: @tournament.id, timer_end: 1.day.ago } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Timer end must be in the future') + end + end + + context 'when timer_end is valid' do + it 'updates the timer_end' do + valid_timer_end = 1.day.from_now.change(usec: 0) + patch :set_timer_end, params: { id: @tournament.id, timer_end: valid_timer_end } + expect(response).to have_http_status(:ok) + expect(@tournament.reload.timer_end).to eq(valid_timer_end) + end + end + end + context 'when timer_end_seconds is provided' do + it 'returns unprocessable entity for invalid seconds format' do + patch :set_timer_end, params: { id: @tournament.id, timer_end_seconds: 'invalid' } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Invalid seconds format') + end + + it 'returns unprocessable entity for negative seconds' do + patch :set_timer_end, params: { id: @tournament.id, timer_end_seconds: -3600 } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Timer end must be in the future') + end + + it 'updates the timer_end with valid seconds' do + valid_timer_end_seconds = 3600 + expected_timer_end = (Time.zone.now + valid_timer_end_seconds).change(usec: 0) + patch :set_timer_end, params: { id: @tournament.id, timer_end_seconds: valid_timer_end_seconds } + expect(response).to have_http_status(:ok) + expect(@tournament.reload.timer_end.change(usec: 0)).to eq(expected_timer_end) + end + end + context 'when both timer_end and timer_end_seconds are provided' do + it 'returns unprocessable entity' do + patch :set_timer_end, params: { id: @tournament.id, timer_end: 1.day.from_now, timer_end_seconds: 3600 } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)).to include('error' => 'Only one of timer_end or timer_end_seconds is allowed') + end + end + end + + describe 'GET #timer_end' do + before do + @request.env['HTTP_ACCEPT'] = 'application/json' + end + + it 'returns success response' do + get :timer_end, params: { id: @tournament.to_param } + expect(response).to have_http_status(:ok) + end + + it 'returns timer_end value' do + @tournament.update(timer_end: Time.zone.now + 1.day) + get :timer_end, params: { id: @tournament.to_param } + expect(JSON.parse(response.body)['timer_end']).to eq(@tournament.timer_end.as_json) + end + + it 'returns nil if timer_end is not set' do + @tournament.update(timer_end: nil) + get :timer_end, params: { id: @tournament.to_param } + expect(JSON.parse(response.body)['timer_end']).to be_nil + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 18608f5..15c095d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -39,6 +39,7 @@ RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" # Run only focused tests + # TODO REVERT ME config.filter_run_when_matching :focus # If you're not using ActiveRecord, or you'd prefer not to run each of your