diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 39dd9c5fc..a1d09d9dc 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -1,5 +1,7 @@ class ApiController < ApplicationController + @@log = Logging.logger[ApiController] + # define common error handlers rescue_from 'JamRuby::StateError' do |exception| @exception = exception @@ -7,14 +9,23 @@ class ApiController < ApplicationController end rescue_from 'JamRuby::JamArgrumentError' do |exception| @exception = exception - render "errors/jam_argument_error.rabl", :status => 500 + render "errors/jam_argument_error", :status => 500 end rescue_from 'JamRuby::PermissionError' do |exception| @exception = exception - render "errors/permission_error.rabl", :status => 500 + render "errors/permission_error", :status => 500 end rescue_from 'ActiveRecord::RecordNotFound' do |exception| - render :json => {:message => exception.message}, :status => 404 + @@log.debug(exception) + render :json => { :errors => { :resource => ["record not found"] } }, :status => 404 end + rescue_from 'PG::Error' do |exception| + @@log.debug(exception) + if exception.to_s.include? "duplicate key value violates unique constraint" + render :json => { :errors => { :resource => ["resource already exists"] } }, :status => 409 # 409 = conflict + else + raise exception + end + end end \ No newline at end of file diff --git a/app/controllers/api_invitation_controller.rb b/app/controllers/api_invitation_controller.rb deleted file mode 100644 index bc1321bab..000000000 --- a/app/controllers/api_invitation_controller.rb +++ /dev/null @@ -1,115 +0,0 @@ -class ApiMusicSessionsController < ApiController - - # have to be signed in currently to see this screen - before_filter :signed_in_user - - respond_to :json - - def initialize - @mq_router = MQRouter.new - @message_factory = MessageFactory.new - - end - - def index - @invitations = Invitation.paginate(page: params[:page]) - end - - def create - ConnectionManager.active_record_transaction do |connection_manager| - - client_id = params[:client_id] - - if client_id.nil? - raise JamArgumentError, "client_id must be specified" - end - - @music_session = MusicSession.new() - @music_session.creator = current_user - @music_session.description = params[:description] - genres = params[:genres] - - - unless genres.nil? - genres.each do |genre| - loaded_genre = Genre.find_by_description!(genre) - @music_session.genres << loaded_genre - end - end - - saved = @music_session.save - - if saved - # auto-join this user into the newly created session - connection_manager.join_music_session(current_user.id, client_id, @music_session.id) - end - end - - if @music_session.errors.any? - # we have to do this because api_session_detail_url will fail with a bad @music_session (or something like it) - response.status = :unprocessable_entity - respond_with @music_session - else - respond_with @music_session, responder: ApiResponder, :location => api_session_detail_url(@music_session) - end - end - - def show - @music_session = MusicSession.find(params[:id]) - end - - def delete - @music_session = MusicSession.find(params[:id]) - @music_session.delete - - respond_with @music_session, responder: ApiResponder - end - - def participant_show - @connection = Connection.find_by_client_id(params[:id]) - end - - def participant_create - @music_session = nil - @connection = nil - - ConnectionManager.active_record_transaction do |connection_manager| - @music_session = MusicSession.find(params[:id]) - - if @music_session.nil? - raise JamArgumentError, "no session found" - end - - client_id = params[:client_id] - - connection_manager.join_music_session(current_user.id, client_id, @music_session.id) - - @connection = Connection.find_by_client_id(client_id) - end - - # send out notification to queue to the rest of the session - user_joined = @message_factory.user_joined_music_session(current_user.id, current_user.name) - @mq_router.user_publish_to_session(@music_session, current_user, user_joined, sender = {:client_id => @connection.client_id}) - - respond_with @connection, responder: ApiResponder, :location => api_session_participant_detail_url(@connection.client_id) - end - - def participant_delete - - ConnectionManager.active_record_transaction do |connection_manager| - @connection = Connection.find_by_client_id(params[:id]) - - if @connection.nil? - raise JamArgumentError, "no client found with specified client_id #{id}" - end - - if @connection.user.id != current_user.id - raise PermissionError, "you do not own this connection" - end - - connection_manager.leave_music_session(current_user.id, @connection.client_id, @connection.music_session_id) - end - - respond_with @connection, responder: ApiResponder - end -end diff --git a/app/controllers/api_invitations_controller.rb b/app/controllers/api_invitations_controller.rb new file mode 100644 index 000000000..df1437455 --- /dev/null +++ b/app/controllers/api_invitations_controller.rb @@ -0,0 +1,73 @@ +class ApiInvitationsController < ApiController + + # have to be signed in currently to see this screen + before_filter :signed_in_user + + respond_to :json + + def initialize + @mq_router = MQRouter.new + @message_factory = MessageFactory.new + + end + + def index + conditions = {} + sender_id = params[:sender] + receiver_id = params[:receiver] + + if !sender_id.nil? + if current_user.id != sender_id + raise PermissionError, "You can only ask for your own sent invitations" + end + + @invitations = Invitation.where(:sender_id => current_user.id) + elsif !receiver_id.nil? + if current_user.id != receiver_id + raise PermissionError, "You can only ask for your own received invitations" + end + + @invitations = Invitation.where(:receiver_id => current_user.id) + else + # default to invitations you've received + @invitations = Invitation.where(:receiver_id => current_user.id) + end + end + + def create + music_session = MusicSession.find(params[:music_session]) + receiver = User.find(params[:receiver]) + sender = current_user + + @invitation = Invitation.new + @invitation.music_session = music_session + @invitation.sender = sender + @invitation.receiver = receiver + + @invitation.save + + unless @invitation.errors.any? + invitation_notification = @message_factory.session_invitation(receiver.id, @invitation.id) + @mq_router.publish_to_user(receiver.id, invitation_notification) + end + + if @invitation.errors.any? + # we have to do this because api_invitation_detail_url will fail with a bad @invitation + response.status = :unprocessable_entity + respond_with @invitation + else + respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation) + end + end + + def show + @invitation = Invitation.find(params[:id], :conditions => ["receiver_id = ? or sender_id = ?", current_user.id, current_user.id]) + end + + def delete + @invitation = Invitation.find(params[:id], :conditions => ["receiver_id = ? or sender_id = ?", current_user.id, current_user.id]) + @invitation.delete + + respond_with @invitation, responder => ApiResponder + end +end diff --git a/app/controllers/api_music_sessions_controller.rb b/app/controllers/api_music_sessions_controller.rb index 0a7d714ee..3514465e9 100644 --- a/app/controllers/api_music_sessions_controller.rb +++ b/app/controllers/api_music_sessions_controller.rb @@ -46,7 +46,7 @@ class ApiMusicSessionsController < ApiController end if @music_session.errors.any? - # we have to do this because api_session_detail_url will fail with a bad @music_session (or something like it) + # we have to do this because api_session_detail_url will fail with a bad @music_session response.status = :unprocessable_entity respond_with @music_session else diff --git a/app/views/api_invitations/create.rabl b/app/views/api_invitations/create.rabl new file mode 100644 index 000000000..3831ae78e --- /dev/null +++ b/app/views/api_invitations/create.rabl @@ -0,0 +1,3 @@ +object @invitation + +extends "api_invitations/invitation" diff --git a/app/views/api_invitations/index.rabl b/app/views/api_invitations/index.rabl new file mode 100644 index 000000000..5f0a35545 --- /dev/null +++ b/app/views/api_invitations/index.rabl @@ -0,0 +1,3 @@ +object @invitations + +extends "api_invitations/invitation" diff --git a/app/views/api_invitations/invitation.rabl b/app/views/api_invitations/invitation.rabl new file mode 100644 index 000000000..a4dd7dc5e --- /dev/null +++ b/app/views/api_invitations/invitation.rabl @@ -0,0 +1,18 @@ +object @invitation + +attributes :id + +child(:sender => :sender) { + attributes :id, :name +} + +child(:receiver => :receiver) { + attributes :id, :name +} + +child(:music_session) { + attributes :id, :description +} + + + diff --git a/app/views/api_invitations/show.rabl b/app/views/api_invitations/show.rabl new file mode 100644 index 000000000..3831ae78e --- /dev/null +++ b/app/views/api_invitations/show.rabl @@ -0,0 +1,3 @@ +object @invitation + +extends "api_invitations/invitation" diff --git a/config/routes.rb b/config/routes.rb index a8b6bc9de..4de040efa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,5 +52,11 @@ SampleApp::Application.routes.draw do # friends match '/users/:id/friends' => 'api_users#friend_index', :via => :get match '/users/:id/friends/:friend_id' => 'api_users#friend_destroy', :via => :delete + + # invitations + match '/invitations/:id' => 'api_invitations#show', :via => :get, :as => 'api_invitation_detail' + match '/invitations/:id' => 'api_invitations#delete', :via => :delete + match '/invitations' => 'api_invitations#index', :via => :get + match '/invitations' => 'api_invitations#create', :via => :post end end diff --git a/spec/factories.rb b/spec/factories.rb index 2fb64bde0..26d30cfe7 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -10,8 +10,19 @@ FactoryGirl.define do end end + factory :music_session, :class => JamRuby::MusicSession do + sequence(:description) { |n| "Music Session #{n}" } + end + factory :connection, :class => JamRuby::Connection do + sequence(:client_id) { |n| "Client#{n}" } + end + + factory :friendship, :class => JamRuby::Friendship do end + factory :invitation, :class => JamRuby::Invitation do + + end end diff --git a/spec/requests/invitations_api_spec.rb b/spec/requests/invitations_api_spec.rb new file mode 100644 index 000000000..2bf446757 --- /dev/null +++ b/spec/requests/invitations_api_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +describe "Invitation API ", :type => :api do + + include Rack::Test::Methods + + subject { page } + + describe "profile page" do + let(:user) { FactoryGirl.create(:user) } + before do + #sign_in user + MusicSession.delete_all + + post '/sessions', "session[email]" => user.email, "session[password]" => user.password + rack_mock_session.cookie_jar["remember_token"].should == user.remember_token + end + + it "list no invitations" do + get '/api/invitations.json' + last_response.body.should eql('[]') + end + + it "invitation requires receiver" do + + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => user) + connection = FactoryGirl.create(:connection, :user => user, :music_session => music_session) + + post '/api/invitations.json', {:music_session => music_session.id} + last_response.status.should eql(404) + end + + it "invitation can only be sent if you belong to the music session and to friends" do + + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + connection = FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + + post '/api/invitations.json', {:music_session => music_session.id, :receiver => other_user.id} + last_response.status.should eql(422) + response = JSON.parse(last_response.body) + response["errors"].should_not == nil + response["errors"]["music_session"][0].should == Invitation::MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION + response["errors"]["receiver"][0].should == Invitation::FRIENDSHIP_REQUIRED_VALIDATION_ERROR + end + + it "should create a invitation" do + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => user, :music_session => music_session) + FactoryGirl.create(:friendship, :user => user, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => user) + + post '/api/invitations.json', {:music_session => music_session.id, :receiver => other_user.id}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(201) + + # and verify that the response is the same as if we use the GET api + response1 = JSON.parse(last_response.body) + get "/api/invitations/#{response1["id"]}.json" + response1.should == JSON.parse(last_response.body) + end + + it "should list invitations" do + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => user, :music_session => music_session) + FactoryGirl.create(:friendship, :user => user, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => user) + invitation = FactoryGirl.create(:invitation, :sender => user, :receiver => other_user, :music_session => music_session) + + # see that there are no invitations sent to us + get '/api/invitations.json' + response = JSON.parse(last_response.body) + response.should == [] + + # then check that there is one invitation sent by us + get '/api/invitations.json?sender=' + user.id + response = JSON.parse(last_response.body) + response.length.should == 1 + response[0]["id"].should == invitation.id + + # create an invitation the other way + invitation = FactoryGirl.create(:invitation, :sender => other_user, :receiver => user, :music_session => music_session) + # see that there is one invitations sent to us + get '/api/invitations.json' + response = JSON.parse(last_response.body) + response.length.should == 1 + response[0]["id"].should == invitation.id + end + + it "should return a already-created error message and 409 response" do + + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => user, :music_session => music_session) + FactoryGirl.create(:friendship, :user => user, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => user) + invitation = FactoryGirl.create(:invitation, :sender => user, :receiver => other_user, :music_session => music_session) + + post '/api/invitations.json', {:music_session => music_session.id, :receiver => other_user.id}.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(409) + response = JSON.parse(last_response.body) + response["errors"]["resource"][0].should == "resource already exists" + + end + + it "should delete" do + + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => user, :music_session => music_session) + FactoryGirl.create(:friendship, :user => user, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => user) + invitation = FactoryGirl.create(:invitation, :sender => user, :receiver => other_user, :music_session => music_session) + + # refind the invitation to make sure the db serves it up + Invitation.find_by_id(invitation.id).should_not == nil + + delete "/api/invitations/#{invitation.id}.json", "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(204) + + # and then verify that the invitation is gone + Invitation.find_by_id(invitation.id).should == nil + end + + it "should delete by deletion of music session" do + + other_user = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => user, :music_session => music_session) + FactoryGirl.create(:friendship, :user => user, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => user) + invitation = FactoryGirl.create(:invitation, :sender => user, :receiver => other_user, :music_session => music_session) + + # refind the invitation to make sure the db serves it up + Invitation.find_by_id(invitation.id).should_not == nil + + delete "/api/sessions/#{music_session.id}.json", "CONTENT_TYPE" => 'application/json' + last_response.status.should eql(204) + + # and then verify that the invitation is gone + Invitation.find_by_id(invitation.id).should == nil + end + + it "should not allow query of invitations not belonging to current user" do + + other_user = FactoryGirl.create(:user) # in the music session + other_user2 = FactoryGirl.create(:user) # in the music session + # starting condition; valid session and current user is already in it + music_session = FactoryGirl.create(:music_session, :creator => other_user) + FactoryGirl.create(:connection, :user => other_user, :music_session => music_session) + FactoryGirl.create(:connection, :user => other_user2, :music_session => music_session) + FactoryGirl.create(:friendship, :user => other_user2, :friend => other_user) + FactoryGirl.create(:friendship, :user => other_user, :friend => other_user2) + invitation = FactoryGirl.create(:invitation, :sender => other_user2, :receiver => other_user, :music_session => music_session) + + + # then check that there is one invitation sent by us + get '/api/invitations.json?sender=' + other_user.id + last_response.status.should eql(500) + response = JSON.parse(last_response.body) + response.should == {"message" => "You can only ask for your own sent invitations","type" => "PermissionError"} + + # also check that a fetch by id doesn't work + get "/api/invitations/#{invitation.id}" + last_response.status.should eql(404) + + # and that delete by id doesn't work + delete "/api/invitations/#{invitation.id}" + last_response.status.should eql(404) + end + end +end