From 58267b6b3affe7d214e9405384ef0ca6e5ca87a8 Mon Sep 17 00:00:00 2001 From: Tobias Huber Date: Mon, 13 Apr 2026 23:30:29 +0200 Subject: [PATCH] Fix timer timezone handling --- app/controllers/tournaments_controller.rb | 66 ++++++++++--------- app/serializers/tournament_serializer.rb | 6 +- .../tournaments_controller_spec.rb | 28 ++++++++ 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 85d1eb3..62ad410 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -96,7 +96,7 @@ class TournamentsController < ApplicationController # GET /tournaments/:id/timer_end def timer_end - render json: { timer_end: @tournament.timer_end } + render json: { timer_end: serialize_timer_end(@tournament.timer_end) } end # PATCH /tournaments/:id/set_timer_end @@ -115,6 +115,10 @@ class TournamentsController < ApplicationController { timer_end: params[:timer_end] } end + def serialize_timer_end(timer_end) + timer_end&.iso8601 + end + def organize_teams_in_groups(teams) # each team gets put into an array of teams depending on the group specified in team[:group] teams.group_by { |team| team['group'] }.values.map do |group| @@ -177,40 +181,42 @@ class TournamentsController < ApplicationController error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match' }, status: :unprocessable_entity end -end -def validate_set_timer_end_params - timer_end = params[:timer_end] - timer_end_seconds = params[:timer_end_seconds] + 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 + # 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 - 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 + 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 - rescue ArgumentError - return render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity + + 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 + + params[:timer_end] = parsed_time + 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 - 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 751aa4f..854ed5c 100644 --- a/app/serializers/tournament_serializer.rb +++ b/app/serializers/tournament_serializer.rb @@ -2,9 +2,13 @@ class TournamentSerializer < SimpleTournamentSerializer attributes :description, :playoff_teams_amount, - :instant_finalists_amount, :intermediate_round_participants_amount, :timer_end + :instant_finalists_amount, :intermediate_round_participants_amount has_many :stages + attribute :timer_end do + object.timer_end&.iso8601 + end + attribute :owner_username do object.owner.username end diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index 92f3597..977b89a 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -142,6 +142,34 @@ RSpec.describe TournamentsController, type: :controller do end end + describe 'GET #timer_end' do + it 'returns timer_end in ISO 8601 format' do + timer_end = Time.utc(2026, 4, 13, 18, 0, 0) + @tournament.update!(timer_end: timer_end) + + get :timer_end, params: { id: @tournament.to_param } + + expect(response).to be_successful + expect(deserialize_response(response)[:timer_end]).to eq(timer_end.iso8601) + end + end + + describe 'PATCH #set_timer_end' do + before(:each) do + apply_authentication_headers_for @user + end + + it 'stores timezone-aware timer_end values without shifting them' do + timer_end = '2030-04-13T18:00:00+02:00' + + patch :set_timer_end, params: { id: @tournament.to_param, timer_end: timer_end } + + expect(response).to be_successful + expect(@tournament.reload.timer_end.iso8601).to eq(Time.iso8601(timer_end).utc.iso8601) + expect(deserialize_response(response)[:timer_end]).to eq(@tournament.timer_end.iso8601) + end + end + describe 'POST #create' do let(:create_playoff_tournament_data) do {