From 0d30886168ea2275f265d4a84b365f21a0e609b9 Mon Sep 17 00:00:00 2001 From: Malaber Date: Tue, 4 Mar 2025 21:59:57 +0100 Subject: [PATCH 01/12] REVERT ME run only focused tests --- Gemfile.lock | 1 + spec/rails_helper.rb | 1 + 2 files changed, 2 insertions(+) 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/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 From 3c1168212e4ab8338f5d2cd8260bb8e6919d7dc8 Mon Sep 17 00:00:00 2001 From: Tobias Huber Date: Sat, 8 Mar 2025 17:08:48 +0100 Subject: [PATCH 02/12] add timer_end to tournaments model --- app/controllers/tournaments_controller.rb | 42 ++++++++++++- config/routes.rb | 4 ++ .../tournaments_controller_spec.rb | 59 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index a40c6bf..19cb4ce 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,19 @@ class TournamentsController < ApplicationController }, status: :unprocessable_entity end end + +def validate_set_timer_end_params + timer_end = params[:timer_end] + return render json: { error: 'Timer end is required' }, status: :unprocessable_entity unless timer_end.present? + + begin + parsed_time = Time.zone.parse(timer_end) + if parsed_time.nil? + render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity + elsif !parsed_time.future? + render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity + end + rescue ArgumentError + render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity + end +end \ No newline at end of file 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/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index ae8f469..e0c4f82 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -477,4 +477,63 @@ RSpec.describe TournamentsController, type: :controller do end end end + + describe '#validate_set_timer_end_params' do + let(:tournament) { create(:tournament) } + + before do + apply_authentication_headers_for tournament.owner + @request.env['HTTP_ACCEPT'] = 'application/json' + end + + 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 'passes validation' do + patch :set_timer_end, params: { id: tournament.id, timer_end: 1.day.from_now } + expect(response).to have_http_status(:ok) + end + end + end + + describe 'GET #timer_end', focus:true do + let(:tournament) { create(:tournament) } + + 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 + end end From 514bbb553b63be0eadf81743f1cc32ce1715923b Mon Sep 17 00:00:00 2001 From: Malaber Date: Sun, 9 Mar 2025 21:27:38 +0100 Subject: [PATCH 03/12] Add migration for timer --- db/migrate/20250309202658_add_timer_end_to_tournaments.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20250309202658_add_timer_end_to_tournaments.rb 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 From 7b9a454cb627992321ec4625089f964aa1b140f5 Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 10 Mar 2025 11:49:51 +0100 Subject: [PATCH 04/12] Fix syntax in tournaments_controller.rb --- app/controllers/tournaments_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 19cb4ce..88c4de8 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class TournamentsController < ApplicationController - before_action :set_tournament, only: %i[show update destroy set_timer_end, timer_end] + 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] From bdcc6e118276fc3eb56d7fd2511223167911f9d3 Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 10 Mar 2025 11:50:25 +0100 Subject: [PATCH 05/12] Fix tests --- .../tournaments_controller_spec.rb | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index e0c4f82..9cd8bc5 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -478,17 +478,15 @@ RSpec.describe TournamentsController, type: :controller do end end - describe '#validate_set_timer_end_params' do - let(:tournament) { create(:tournament) } - - before do - apply_authentication_headers_for tournament.owner + describe 'PATCH #set_timer_end' do + before(:each) do + apply_authentication_headers_for @tournament.owner @request.env['HTTP_ACCEPT'] = 'application/json' end context 'when timer_end is missing' do it 'returns unprocessable entity' do - patch :set_timer_end, params: { id: tournament.id } + 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 @@ -496,7 +494,7 @@ RSpec.describe TournamentsController, type: :controller do context 'when timer_end is invalid datetime' do it 'returns unprocessable entity' do - patch :set_timer_end, params: { id: tournament.id, timer_end: 'invalid' } + 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 @@ -504,36 +502,42 @@ RSpec.describe TournamentsController, type: :controller do 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 } + 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 'passes validation' do - patch :set_timer_end, params: { id: tournament.id, timer_end: 1.day.from_now } + 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 - describe 'GET #timer_end', focus:true do - let(:tournament) { create(:tournament) } - + 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 } + 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) + @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 From 4d1e2c5164739992f651277dc02f82cb589743ec Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 10 Mar 2025 12:37:19 +0100 Subject: [PATCH 06/12] Add option to also pass seconds to timer_end --- app/controllers/tournaments_controller.rb | 38 +++++++--- .../tournaments_controller_spec.rb | 69 +++++++++++++------ 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 88c4de8..6cab8e9 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -181,16 +181,32 @@ end def validate_set_timer_end_params timer_end = params[:timer_end] - return render json: { error: 'Timer end is required' }, status: :unprocessable_entity unless timer_end.present? + timer_end_seconds = params[:timer_end_seconds] - begin - parsed_time = Time.zone.parse(timer_end) - if parsed_time.nil? - render json: { error: 'Invalid datetime format' }, status: :unprocessable_entity - elsif !parsed_time.future? - render json: { error: 'Timer end must be in the future' }, status: :unprocessable_entity - end - rescue ArgumentError - render json: { error: 'Invalid datetime 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 -end \ No newline at end of file + + if timer_end_seconds.present? + begin + parsed_time = Time.zone.now + timer_end_seconds.to_i + params[:timer_end] = parsed_time + rescue ArgumentError + return render json: { error: 'Invalid seconds format' }, status: :unprocessable_entity + end + 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/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index 9cd8bc5..5941ecd 100644 --- a/spec/controllers/tournaments_controller_spec.rb +++ b/spec/controllers/tournaments_controller_spec.rb @@ -483,37 +483,66 @@ RSpec.describe TournamentsController, type: :controller 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 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') + 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 is invalid datetime' do - it 'returns unprocessable entity' do - patch :set_timer_end, params: { id: @tournament.id, timer_end: 'invalid' } + 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 datetime format') + expect(JSON.parse(response.body)).to include('error' => 'Invalid seconds 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 } + 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 - 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 } + 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).to eq(valid_timer_end) + 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 From 16e65a3b0344eca945ced506130fc903b0e82190 Mon Sep 17 00:00:00 2001 From: Malaber Date: Mon, 10 Mar 2025 12:56:20 +0100 Subject: [PATCH 07/12] Fix missing checks --- app/controllers/tournaments_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 6cab8e9..2e33843 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -190,11 +190,15 @@ def validate_set_timer_end_params if timer_end_seconds.present? begin - parsed_time = Time.zone.now + timer_end_seconds.to_i - params[:timer_end] = parsed_time + 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) From 5ab18063dd0ca80874d101c21330a9aae47c301b Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 13 Mar 2025 12:34:05 +0100 Subject: [PATCH 08/12] Fix bug where uneven groups cause 500 when requesting beamer view --- app/controllers/matches_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index 2acf65f..90e0c8d 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -15,6 +15,8 @@ class MatchesController < ApplicationController elsif match_params['state'] == 'upcoming' # for every group within the tournament find the match with the lowest position that is of state 'not_started' upcoming_matches = @tournament.stages.find_by(level: -1)&.groups&.map { |g| g.matches.select { |m| m.state == 'not_started' }.min_by(&:position) } + # filter out nil values (this may happen if one of the groups already has no upcoming matches) + upcoming_matches = upcoming_matches.reject(&:nil?) # if there are none, the group stage is over, so we have to look into the playoff stages if upcoming_matches.nil? next_level = 0 From fafc4d393770324dc8e5288c950e08061b908fae Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 13 Mar 2025 12:35:27 +0100 Subject: [PATCH 09/12] Rubocop --- app/controllers/matches_controller.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index 90e0c8d..826043c 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -36,14 +36,12 @@ class MatchesController < ApplicationController m.state == match_params['state'] end end - render json: matches, each_serializer: ExtendedMatchSerializer, include: [ - 'match_scores.team', 'bets', 'stage', 'group' - ] + render json: matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group] end # GET /matches/1 def show - render json: @match, include: ['match_scores.points', 'match_scores.team', 'bets'] + render json: @match, include: %w[match_scores.points match_scores.team bets] end # PATCH/PUT /matches/1 From 41fbf654df6b363154b71692b5d91098a21fb514 Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 13 Mar 2025 12:41:48 +0100 Subject: [PATCH 10/12] Add todo --- spec/controllers/matches_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/matches_controller_spec.rb b/spec/controllers/matches_controller_spec.rb index ec9c725..1beabe4 100644 --- a/spec/controllers/matches_controller_spec.rb +++ b/spec/controllers/matches_controller_spec.rb @@ -43,6 +43,7 @@ RSpec.describe MatchesController, type: :controller do expect(body.empty?).to be true end end + # TODO add test for upcoming once there is test data for a "valid" group stage end describe 'GET #show' do From f7dc8411f4f9489b39d0b3cdbffdff7d841f741d Mon Sep 17 00:00:00 2001 From: Malaber Date: Thu, 13 Mar 2025 15:56:01 +0100 Subject: [PATCH 11/12] Include timer_end in TournamentSerializer --- app/serializers/tournament_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 9f1807ab4bbe6d3a2f9e0162a569ec9824f68999 Mon Sep 17 00:00:00 2001 From: Malaber Date: Fri, 14 Mar 2025 12:47:06 +0100 Subject: [PATCH 12/12] Test timer_end is in show tournament --- spec/controllers/tournaments_controller_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb index 5941ecd..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