diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 803d135..680289d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,4 +24,8 @@ class ApplicationController < ActionController::API ] }, status: :forbidden end + + def deserialize_params(opts) + ActiveModelSerializers::Deserialization.jsonapi_parse(params, opts) + end end diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index a00c1c7..6dea146 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -5,10 +5,4 @@ class MatchesController < ApplicationController def show render json: Match.find(params[:id]), include: ['scores.score', 'scores.team'], status: status end - - private - - def match_params - ActiveModelSerializers::Deserialization.jsonapi_parse(params, only: [:state]) - end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index fde8269..b61d202 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -26,6 +26,6 @@ class TeamsController < ApplicationController end def team_params - ActiveModelSerializers::Deserialization.jsonapi_parse(params, only: [:name]) + deserialize_params only: %i[name] end end diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb new file mode 100644 index 0000000..a8710e1 --- /dev/null +++ b/app/controllers/tournaments_controller.rb @@ -0,0 +1,53 @@ +# 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] + + # GET /tournaments + def index + tournaments = Tournament.where(public: true).or(Tournament.where(owner: current_user)).order(:created_at) + render json: tournaments, each_serializer: SimpleTournamentSerializer + end + + # GET /tournaments/1 + def show + render json: @tournament + end + + # POST /tournaments + def create + tournament = current_user.tournaments.new tournament_params + + if tournament.save + render json: tournament, status: :created, location: tournament + else + render json: tournament.errors, status: :unprocessable_entity + end + end + + # PATCH/PUT /tournaments/1 + def update + if @tournament.update(tournament_params) + render json: @tournament + else + render json: @tournament.errors, status: :unprocessable_entity + end + end + + # DELETE /tournaments/1 + def destroy + @tournament.destroy + end + + private + + def set_tournament + @tournament = Tournament.find(params[:id]) + end + + def tournament_params + deserialize_params only: %i[name description public teams] + end +end diff --git a/app/models/team.rb b/app/models/team.rb index 8738d12..925ce9f 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Team < ApplicationRecord - belongs_to :tournament + belongs_to :tournament, optional: true has_many :group_scores, dependent: :destroy has_many :scores, dependent: :destroy diff --git a/app/serializers/simple_tournament_serializer.rb b/app/serializers/simple_tournament_serializer.rb new file mode 100644 index 0000000..c357f36 --- /dev/null +++ b/app/serializers/simple_tournament_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SimpleTournamentSerializer < ApplicationSerializer + attributes :name, :code, :description, :public +end diff --git a/app/serializers/tournament_serializer.rb b/app/serializers/tournament_serializer.rb new file mode 100644 index 0000000..4c7c2c6 --- /dev/null +++ b/app/serializers/tournament_serializer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class TournamentSerializer < SimpleTournamentSerializer + has_many :teams + has_many :stages +end diff --git a/config/routes.rb b/config/routes.rb index b52f9d6..12e7419 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,4 +5,5 @@ Rails.application.routes.draw do resources :matches, only: %i[show] resources :teams, only: %i[show update] + resources :tournaments end diff --git a/spec/auth_helpers.rb b/spec/auth_helpers.rb index 27869d4..ea55d1f 100644 --- a/spec/auth_helpers.rb +++ b/spec/auth_helpers.rb @@ -3,6 +3,6 @@ module AuthHelpers def apply_authentication_headers_for(user) user_headers = user.create_new_auth_token - @request.headers.merge!(user_headers) + request.headers.merge!(user_headers) end end diff --git a/spec/controllers/matches_controller_spec.rb b/spec/controllers/matches_controller_spec.rb index 47fc9fb..5b83fa8 100644 --- a/spec/controllers/matches_controller_spec.rb +++ b/spec/controllers/matches_controller_spec.rb @@ -3,16 +3,6 @@ require 'rails_helper' RSpec.describe MatchesController, type: :controller do - let(:valid_attributes) do - skip('Add a hash of attributes valid for your model') - end - - let(:invalid_attributes) do - skip('Add a hash of attributes invalid for your model') - end - - let(:valid_session) { {} } - before do @match = create(:match) @match.scores = create_pair(:score) diff --git a/spec/controllers/tournaments_controller_spec.rb b/spec/controllers/tournaments_controller_spec.rb new file mode 100644 index 0000000..d6bec62 --- /dev/null +++ b/spec/controllers/tournaments_controller_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TournamentsController, type: :controller do + def tournament_ids(response) + deserialize_list(response).map { |t| t[:id].to_i } + end + before do + @tournament = create(:tournament) + @user = @tournament.owner + @another_user = create(:user) + @private_tournament = create(:tournament, user: @another_user, public: false) + @teams = create_list(:detached_team, 4) + end + + describe 'GET #index' do + it 'returns a success response' do + get :index + expect(response).to be_successful + end + + it 'returns all public tournaments' do + get :index + tournaments = deserialize_list response + public_tournaments = tournaments.select { |t| t[:public] } + expect(public_tournaments.size).to eq((Tournament.where public: true).size) + end + + it 'returns no private tournaments for unauthenticated users' do + get :index + tournaments = deserialize_list response + private_tournaments = tournaments.reject { |t| t[:public] } + expect(private_tournaments.size).to eq(0) + end + + it 'returns private tournaments owned by the authenticated user' do + apply_authentication_headers_for @another_user + get :index + expect(tournament_ids(response)).to include(@private_tournament.id) + end + + it 'returns no private tournaments owned by another user' do + apply_authentication_headers_for @user + get :index + expect(tournament_ids(response)).not_to include(@private_tournament.id) + end + end + + describe 'GET #show' do + it 'returns a success response' do + get :show, params: { id: @tournament.to_param } + expect(response).to be_successful + end + + it 'returns the requested tournament' do + get :show, params: { id: @tournament.to_param } + expect(deserialize_response(response)[:id].to_i).to eq(@tournament.id) + end + end + + describe 'POST #create' do + let(:create_data) do + { + data: { + type: 'tournaments', + attributes: { + name: Faker::Dog.name, + description: Faker::Lorem.sentence, + public: false + }, + relationships: { + teams: { + data: @teams.map { |team| { type: 'teams', id: team.id } } + } + } + } + } + end + + before(:each) do + apply_authentication_headers_for @user + end + + context 'with valid params' do + it 'creates a new Tournament' do + expect do + post :create, params: create_data + end.to change(Tournament, :count).by(1) + end + + it 'associates the new tournament with the authenticated user' do + expect do + post :create, params: create_data + end.to change(@user.tournaments, :size).by(1) + end + + it 'associates the given teams with the created tournament' do + new_teams = create_list(:detached_team, 4) + new_teams_create_data = create_data + new_teams_create_data[:data][:relationships][:teams][:data] = \ + new_teams.map { |team| { type: 'teams', id: team.id } } + post :create, params: new_teams_create_data + expect(Tournament.last.teams).to match_array(new_teams) + end + + it 'renders a JSON response with the new tournament' do + post :create, params: create_data + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json') + expect(response.location).to eq(tournament_url(Tournament.last)) + end + end + end + + describe 'PUT #update' do + let(:valid_update) do + { + data: { + type: 'tournaments', + id: @tournament.id, + attributes: { + name: Faker::Dog.name + } + } + } + end + + context 'with valid params' do + context 'without authentication headers' do + it 'renders a unauthorized error response' do + put :update, params: { id: @tournament.to_param }.merge(valid_update) + expect(response).to have_http_status(:unauthorized) + end + end + + context 'as owner' do + before(:each) do + apply_authentication_headers_for @tournament.owner + end + + it 'updates the requested tournament' do + put :update, params: { id: @tournament.to_param }.merge(valid_update) + @tournament.reload + expect(@tournament.name).to eq(valid_update[:data][:attributes][:name]) + end + + it 'renders a JSON response with the tournament' do + put :update, params: { id: @tournament.to_param }.merge(valid_update) + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json') + end + end + + context 'as another user' do + before do + apply_authentication_headers_for create(:user) + end + + it 'renders a forbidden error response' do + put :update, params: { id: @tournament.to_param }.merge(valid_update) + expect(response).to have_http_status(:forbidden) + end + end + end + end + + describe 'DELETE #destroy' do + context 'without authentication headers' do + it 'renders a unauthorized error response' do + delete :destroy, params: { id: @tournament.to_param } + expect(response).to have_http_status(:unauthorized) + end + end + + context 'as owner' do + before(:each) do + apply_authentication_headers_for @tournament.owner + end + + it 'destroys the requested tournament' do + expect do + delete :destroy, params: { id: @tournament.to_param } + end.to change(Tournament, :count).by(-1) + end + + it 'destroys associated teams' do + expect do + delete :destroy, params: { id: @tournament.to_param } + end.to change(Team, :count).by(-@tournament.teams.size) + end + end + + context 'as another user' do + before do + apply_authentication_headers_for create(:user) + end + + it 'renders a forbidden error response' do + delete :destroy, params: { id: @tournament.to_param } + expect(response).to have_http_status(:forbidden) + end + end + end +end diff --git a/spec/deserialize_helpers.rb b/spec/deserialize_helpers.rb index 2be5bf4..a6837b4 100644 --- a/spec/deserialize_helpers.rb +++ b/spec/deserialize_helpers.rb @@ -4,4 +4,10 @@ module DeserializeHelpers def deserialize_response(response) ActiveModelSerializers::Deserialization.jsonapi_parse(JSON.parse(response.body)) end + + def deserialize_list(response) + JSON.parse(response.body, symbolize_names: true)[:data].map do |raw_obj| + raw_obj[:attributes].merge raw_obj.except(:attributes) + end + end end diff --git a/spec/factories/teams.rb b/spec/factories/teams.rb index 9836e0f..ba32f6d 100644 --- a/spec/factories/teams.rb +++ b/spec/factories/teams.rb @@ -5,4 +5,8 @@ FactoryBot.define do name { Faker::Dog.name } tournament end + + factory :detached_team, class: Team do + name { Faker::Dog.name } + end end diff --git a/spec/routing/tournaments_routing_spec.rb b/spec/routing/tournaments_routing_spec.rb new file mode 100644 index 0000000..f8d1acf --- /dev/null +++ b/spec/routing/tournaments_routing_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TournamentsController, type: :routing do + describe 'routing' do + it 'routes to #index' do + expect(get: '/tournaments').to route_to('tournaments#index') + end + + it 'routes to #show' do + expect(get: '/tournaments/1').to route_to('tournaments#show', id: '1') + end + + it 'routes to #create' do + expect(post: '/tournaments').to route_to('tournaments#create') + end + + it 'routes to #update via PUT' do + expect(put: '/tournaments/1').to route_to('tournaments#update', id: '1') + end + + it 'routes to #update via PATCH' do + expect(patch: '/tournaments/1').to route_to('tournaments#update', id: '1') + end + + it 'routes to #destroy' do + expect(delete: '/tournaments/1').to route_to('tournaments#destroy', id: '1') + end + end +end