Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BE | Refactor SessionsController#create & #destroy Endpoints #68

Merged
merged 7 commits into from
Oct 30, 2023
32 changes: 15 additions & 17 deletions app/controllers/api/v1/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 22 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions app/exceptions/invalid_authentication_exception.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class InvalidAuthenticationException < StandardError
end
2 changes: 2 additions & 0 deletions app/exceptions/missing_authentication_exception.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class MissingAuthenticationException < StandardError
end
2 changes: 2 additions & 0 deletions app/exceptions/unauthorized_exception.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class UnauthorizedException < StandardError
end
89 changes: 55 additions & 34 deletions spec/requests/api/v1/sessions_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]", # 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: "[email protected]",
zip_code: 12345,
phone_number: 5500009999
)

login_data = {
email: "[email protected]",
password: "PuzzleQueen1"
Expand All @@ -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: "[email protected]",
zip_code: 12345,
phone_number: 5550009999
)

login_data = {
email: "[email protected]",
password: "Queen_of_Puzzles"
Expand All @@ -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
Expand All @@ -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