diff --git a/app/controllers/api/v1/sessions_controller.rb b/app/controllers/api/v1/sessions_controller.rb index 4eecb75..404e14c 100644 --- a/app/controllers/api/v1/sessions_controller.rb +++ b/app/controllers/api/v1/sessions_controller.rb @@ -3,30 +3,28 @@ class Api::V1::SessionsController < ApplicationController before_action :authorize, only: [:destroy] def create - if params[:email].present? && params[:password].present? - returning_user = User.find_by(email: params[:email]) - - # the & is called a "safe navigation" operator - # It prevent a "NoMethodError" from being raised when invoking a method on nil - if returning_user&.authenticate(params[:password]) - session[:user_id] = returning_user.id - render json: UserSerializer.new(returning_user), status: :created - else - render json: { error: 'Invalid email or password' }, status: :unauthorized - end - else - render json: { error: 'Email and password are required' }, status: :unprocessable_entity - end + authenticate_user + session[:user_id] = @returning_user.id + render json: UserSerializer.new(@returning_user), status: :created end def destroy session.delete(:user_id) - head :no_content + head :no_content # Ensures that the client receives an HTTP status code of 204 No Content along with an empty response body end private def authorize - render json: { error: 'Not authorized' }, status: :unauthorized unless session[:user_id] + raise UnauthorizedException if params[:user_id].to_i != session[:user_id] + end + + def authenticate_user + if params[:email].present? && params[:password].present? + @returning_user = User.find_by(email: params[:email]) + raise InvalidAuthenticationException unless @returning_user&.authenticate(params[:password]) # The & is a 'safe navigation operator' which allows you to call a method on an object only if that object is not nil + else + raise MissingAuthenticationException + end end -end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 890c760..07d1a43 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,14 @@ class ApplicationController < ActionController::API rescue_from NoPuzzlesFoundException, with: :no_puzzles_found rescue_from PuzzleNotAvailableException, with: :puzzle_not_available rescue_from NoLoanUpdateException, with: :no_loan_update + rescue_from InvalidAuthenticationException, with: :invalid_authentication + rescue_from MissingAuthenticationException, with: :missing_authentication + rescue_from UnauthorizedException, with: :unauthorized + + + def set_current_user + @current_user ||= User.find_by(id: session[:user_id]) + end def record_not_found(exception) render json: ErrorSerializer.new(exception, 404).serializable_hash, status: :not_found # 404 @@ -26,9 +34,21 @@ def no_loan_update render json: ErrorSerializer.new(error, 404).serializable_hash, status: :not_found # 404 end - def set_current_user - @current_user ||= User.find_by(id: session[:user_id]) + def invalid_authentication + error = InvalidAuthenticationException.new("Invalid email or password.") + render json: ErrorSerializer.new(error, 401).serializable_hash, status: :unauthorized # 401 + end + + def missing_authentication + error = MissingAuthenticationException.new("Email and password are required.") + render json: ErrorSerializer.new(error, 401).serializable_hash, status: :unauthorized # 401 end + + def unauthorized + error = UnauthorizedException.new("Not Authorized.") + render json: ErrorSerializer.new(error, 401).serializable_hash, status: :unauthorized # 401 + end + end # Note to self: Saving this to remember process that lead me to final version: diff --git a/app/exceptions/invalid_authentication_exception.rb b/app/exceptions/invalid_authentication_exception.rb new file mode 100644 index 0000000..4c3a5a6 --- /dev/null +++ b/app/exceptions/invalid_authentication_exception.rb @@ -0,0 +1,2 @@ +class InvalidAuthenticationException < StandardError +end \ No newline at end of file diff --git a/app/exceptions/missing_authentication_exception.rb b/app/exceptions/missing_authentication_exception.rb new file mode 100644 index 0000000..4be74ef --- /dev/null +++ b/app/exceptions/missing_authentication_exception.rb @@ -0,0 +1,2 @@ +class MissingAuthenticationException < StandardError +end \ No newline at end of file diff --git a/app/exceptions/unauthorized_exception.rb b/app/exceptions/unauthorized_exception.rb new file mode 100644 index 0000000..62b7b90 --- /dev/null +++ b/app/exceptions/unauthorized_exception.rb @@ -0,0 +1,2 @@ +class UnauthorizedException < StandardError +end \ No newline at end of file diff --git a/spec/requests/api/v1/sessions_request_spec.rb b/spec/requests/api/v1/sessions_request_spec.rb index 02263cb..555010e 100644 --- a/spec/requests/api/v1/sessions_request_spec.rb +++ b/spec/requests/api/v1/sessions_request_spec.rb @@ -2,17 +2,19 @@ RSpec.describe 'SessionsController' do describe '#create' do + before(:each) do + @user = User.create( + full_name: "Diana Puzzler", + password: "PuzzleQueen1", + password_confirmation: "PuzzleQueen1", + email: "dpuzzler@myemail.com", # The Users#create will format the email in this way before it is saved to the db + zip_code: 12345, + phone_number: "(555) 000-9999" # The Users#create will format the phone_number in this way before it is saved to the db + ) + end + context 'when successful' do it 'logs in a user' do - user = User.create( - full_name: "Diana Puzzler", - password: "PuzzleQueen1", - password_confirmation: "PuzzleQueen1", - email: "dpuzzler@myemail.com", - zip_code: 12345, - phone_number: 5500009999 - ) - login_data = { email: "dpuzzler@myemail.com", password: "PuzzleQueen1" @@ -22,21 +24,14 @@ post "/api/v1/login", headers:, params: JSON.generate(login_data) expect(response).to have_http_status(201) - expect(session[:user_id]).to eq(user.id) + parsed_data = JSON.parse(response.body, symbolize_names: true) + + expect(session[:user_id]).to eq(@user.id) end end context 'when NOT successful' do it 'cannot log in a user with an incorrect password' do - user = User.create( - full_name: "Diana Puzzler", - password: "PuzzleQueen1", - password_confirmation: "PuzzleQueen1", - email: "dpuzzler@myemail.com", - zip_code: 12345, - phone_number: 5550009999 - ) - login_data = { email: "dpuzzler@myemail.com", password: "Queen_of_Puzzles" @@ -46,12 +41,38 @@ post "/api/v1/login", headers:, params: JSON.generate(login_data) expect(response).to have_http_status(401) + parsed_error_data = JSON.parse(response.body, symbolize_names: true) + + expect(parsed_error_data).to be_a(Hash) + expect(parsed_error_data.keys).to eq([:errors]) + expect(parsed_error_data[:errors]).to be_an(Array) + expect(parsed_error_data[:errors][0]).to be_a(Hash) + expect(parsed_error_data[:errors][0].keys).to eq([:status, :title, :detail]) + expect(parsed_error_data[:errors][0][:status]).to eq("401") + expect(parsed_error_data[:errors][0][:title]).to eq("InvalidAuthenticationException") + expect(parsed_error_data[:errors][0][:detail]).to eq("Invalid email or password.") + end + it 'cannot log in a user with a missing email and password' do + login_data = { + email: nil, + password: nil + } + + headers = { 'CONTENT_TYPE' => 'application/json' } + post "/api/v1/login", headers:, params: JSON.generate(login_data) + + expect(response).to have_http_status(401) parsed_error_data = JSON.parse(response.body, symbolize_names: true) expect(parsed_error_data).to be_a(Hash) - expect(parsed_error_data.keys).to eq([:error]) - expect(parsed_error_data[:error]).to eq("Invalid email or password") + expect(parsed_error_data.keys).to eq([:errors]) + expect(parsed_error_data[:errors]).to be_an(Array) + expect(parsed_error_data[:errors][0]).to be_a(Hash) + expect(parsed_error_data[:errors][0].keys).to eq([:status, :title, :detail]) + expect(parsed_error_data[:errors][0][:status]).to eq("401") + expect(parsed_error_data[:errors][0][:title]).to eq("MissingAuthenticationException") + expect(parsed_error_data[:errors][0][:detail]).to eq("Email and password are required.") end end end @@ -76,23 +97,23 @@ end context 'when NOT successful' do - # Unsure how to test this: + it 'cannot delete a user session of another user' do + user_1 = create(:user) + user_2 = create(:user) + login_data = { email: user_1.email, password: user_1.password } - # it 'cannot delete a user session of another user' do - # user = create(:user) - # login_data = { email: user.email, password: user.password } - - # headers = { 'CONTENT_TYPE' => 'application/json' } - # post "/api/v1/users/#{user.id}/login", headers:, params: JSON.generate(login_data) + headers = { 'CONTENT_TYPE' => 'application/json' } + post "/api/v1/login", headers:, params: JSON.generate(login_data) - # expect(response).to have_http_status(201) - # expect(session[:user_id]).to eq(user.id) + expect(response).to have_http_status(201) + expect(session[:user_id]).to eq(user_1.id) + session_token_user1 = JSON.parse(response.body)['session_token'] - # delete "/api/v1/users/007/logout" + delete "/api/v1/users/#{user_2.id}/logout", headers: { 'Authorization' => "Bearer #{session_token_user1}" } - # expect(response).to have_http_status(401) - # expect(session[:user_id]).to eq(user.id) - # end + expect(response).to have_http_status(401) + expect(session[:user_id]).to eq(user_1.id) + end end end end