Skip to content

Commit

Permalink
Merge pull request #68 from WWC-Hackathon-2023/3refactor
Browse files Browse the repository at this point in the history
BE | Refactor `SessionsController#create` & `#destroy` Endpoints
  • Loading branch information
MelTravelz authored Oct 30, 2023
2 parents 24a25c3 + bf71c2d commit c7eded6
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 53 deletions.
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: "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"
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: "dpuzzler@myemail.com",
zip_code: 12345,
phone_number: 5550009999
)

login_data = {
email: "dpuzzler@myemail.com",
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

0 comments on commit c7eded6

Please sign in to comment.