Skip to content

Commit

Permalink
⚡️ Implemented /auth/login/v1 endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
lhbelfanti committed Jan 13, 2025
1 parent ce64a20 commit a76c695
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 27 deletions.
9 changes: 9 additions & 0 deletions cmd/api/auth/dtos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package auth

import "time"

// LoginResponse represents the response of the LogIn endpoint
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
63 changes: 55 additions & 8 deletions cmd/api/auth/handler.go
Original file line number Diff line number Diff line change
@@ -1,45 +1,92 @@
package auth

import (
"ahbcc/internal/log"
"encoding/json"
"errors"
"net/http"

"ahbcc/cmd/api/user"
"ahbcc/internal/log"
)

// SignUpHandlerV1 HTTP Handler of the endpoint /auth/signup
// SignUpHandlerV1 HTTP Handler of the endpoint /auth/signup/v1
func SignUpHandlerV1(signUp SignUp) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var user user.DTO
err := json.NewDecoder(r.Body).Decode(&user)
var userDTO user.DTO
err := json.NewDecoder(r.Body).Decode(&userDTO)
if err != nil {
log.Error(ctx, err.Error())
http.Error(w, InvalidRequestBody, http.StatusBadRequest)
return
}
ctx = log.With(ctx, log.Param("username", user.Username))
ctx = log.With(ctx, log.Param("username", userDTO.Username))

err = validateBody(user)
err = validateBody(userDTO)
if err != nil {
log.Error(ctx, err.Error())
http.Error(w, InvalidRequestBody, http.StatusBadRequest)
}

err = signUp(ctx, user)
err = signUp(ctx, userDTO)
if err != nil {
log.Error(ctx, err.Error())
http.Error(w, FailedToSignUp, http.StatusInternalServerError)
return
}

log.Info(ctx, "User successfully signed up")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("User successfully signed up"))
}
}

// LogInV1 HTTP Handler of the endpoint /auth/login/v1
func LogInV1(logIn LogIn) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var userDTO user.DTO
err := json.NewDecoder(r.Body).Decode(&userDTO)
if err != nil {
log.Error(ctx, err.Error())
http.Error(w, InvalidRequestBody, http.StatusBadRequest)
return
}

err = validateBody(userDTO)
if err != nil {
log.Error(ctx, err.Error())
http.Error(w, InvalidRequestBody, http.StatusBadRequest)
}

token, expiresAt, err := logIn(ctx, userDTO)
if err != nil {
log.Error(ctx, err.Error())

switch {
case errors.Is(err, FailedToLoginDueWrongPassword):
http.Error(w, FailedToSignUp, http.StatusUnauthorized)
return
default:
http.Error(w, FailedToSignUp, http.StatusInternalServerError)
return
}
}

loginResponse := LoginResponse{
Token: token,
ExpiresAt: expiresAt,
}

log.Info(ctx, "User successfully logged in")
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(loginResponse)
}
}

// validateBody validates that mandatory fields are present
func validateBody(user user.DTO) error {
if user.Username == "" {
Expand Down
87 changes: 87 additions & 0 deletions cmd/api/auth/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"

Expand Down Expand Up @@ -88,3 +89,89 @@ func TestSignUpHandlerV1_failsWhenSignUpThrowsError(t *testing.T) {

assert.Equal(t, want, got)
}

func TestLoginHandlerV1_success(t *testing.T) {
mockExpiresAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockLogIn := auth.MockLogIn("abcd", mockExpiresAt, nil)
mockResponseWriter := httptest.NewRecorder()
mockUser := user.MockDTO()
mockBody, _ := json.Marshal(mockUser)
mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/auth/login/v1", bytes.NewReader(mockBody))

logInHandlerV1 := auth.LogInV1(mockLogIn)

logInHandlerV1(mockResponseWriter, mockRequest)

want := http.StatusOK
got := mockResponseWriter.Result().StatusCode

assert.Equal(t, want, got)
}

func TestLoginHandlerV1_failsWhenTheBodyCantBeParsed(t *testing.T) {
mockExpiresAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockLogIn := auth.MockLogIn("abcd", mockExpiresAt, nil)
mockResponseWriter := httptest.NewRecorder()
mockBody, _ := json.Marshal(`{"wrong": "body"}`)
mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/auth/login/v1", bytes.NewReader(mockBody))

logInHandlerV1 := auth.LogInV1(mockLogIn)

logInHandlerV1(mockResponseWriter, mockRequest)

want := http.StatusBadRequest
got := mockResponseWriter.Result().StatusCode

assert.Equal(t, want, got)
}

func TestLoginHandlerV1_failsWhenValidateBodyThrowsError(t *testing.T) {
mockExpiresAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockLogIn := auth.MockLogIn("abcd", mockExpiresAt, nil)
mockResponseWriter := httptest.NewRecorder()

for _, test := range []struct {
mockUser user.DTO
}{
{mockUser: user.DTO{Username: "username"}},
{mockUser: user.DTO{Password: "password"}},
} {
mockBody, _ := json.Marshal(test.mockUser)
mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/auth/login/v1", bytes.NewReader(mockBody))

logInHandlerV1 := auth.LogInV1(mockLogIn)

logInHandlerV1(mockResponseWriter, mockRequest)

want := http.StatusBadRequest
got := mockResponseWriter.Result().StatusCode

assert.Equal(t, want, got)
}
}

func TestLoginHandlerV1_failsWhenLogInThrowsError(t *testing.T) {
for _, test := range []struct {
logInError error
want int
}{
{logInError: auth.FailedToLoginDueWrongPassword, want: http.StatusUnauthorized},
{logInError: errors.New("failed to log in"), want: http.StatusInternalServerError},
} {
mockLogIn := auth.MockLogIn("", time.Time{}, test.logInError)
mockResponseWriter := httptest.NewRecorder()
mockUser := user.MockDTO()
mockBody, _ := json.Marshal(mockUser)
mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/auth/login/v1", bytes.NewReader(mockBody))

logInHandlerV1 := auth.LogInV1(mockLogIn)

logInHandlerV1(mockResponseWriter, mockRequest)

want := test.want
got := mockResponseWriter.Result().StatusCode

assert.Equal(t, want, got)
}

}
8 changes: 4 additions & 4 deletions cmd/api/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
"ahbcc/internal/log"
)

// Login logs the user in
type Login func(ctx context.Context, user user.DTO) (string, time.Time, error)
// LogIn logs the user in
type LogIn func(ctx context.Context, user user.DTO) (string, time.Time, error)

// MakeLogin creates a new Login function
func MakeLogin(selectUserByUsername user.SelectByUsername, createSessionToken session.CreateToken) Login {
// MakeLogIn creates a new LogIn function
func MakeLogIn(selectUserByUsername user.SelectByUsername, createSessionToken session.CreateToken) LogIn {
return func(ctx context.Context, user user.DTO) (string, time.Time, error) {
userDAO, err := selectUserByUsername(ctx, user.Username)
if err != nil {
Expand Down
30 changes: 15 additions & 15 deletions cmd/api/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,65 @@ import (
"ahbcc/cmd/api/user/session"
)

func TestLogin_success(t *testing.T) {
func TestLogIn_success(t *testing.T) {
mockUserDAO := user.MockDAO()
mockSelectUserByUsername := user.MockSelectByUsername(mockUserDAO, nil)
mockToken := "abcd"
mockCreatedAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockCreateSessionToken := session.MockCreateToken(mockToken, mockCreatedAt, nil)
mockExpiresAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockCreateSessionToken := session.MockCreateToken(mockToken, mockExpiresAt, nil)
mockUserDTO := user.MockDTO()

login := auth.MakeLogin(mockSelectUserByUsername, mockCreateSessionToken)
logIn := auth.MakeLogIn(mockSelectUserByUsername, mockCreateSessionToken)

token, createdAt, err := login(context.Background(), mockUserDTO)
token, expiresAt, err := logIn(context.Background(), mockUserDTO)

assert.Nil(t, err)
assert.Equal(t, mockToken, token)
assert.Equal(t, mockCreatedAt, createdAt)
assert.Equal(t, mockExpiresAt, expiresAt)
}

func TestLogin_failsWhenSelectUserByUsernameThrowsError(t *testing.T) {
func TestLogIn_failsWhenSelectUserByUsernameThrowsError(t *testing.T) {
mockUserDAO := user.MockDAO()
mockSelectUserByUsername := user.MockSelectByUsername(mockUserDAO, errors.New("error while executing SelectByUsername"))
mockToken := "abcd"
mockCreatedAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockCreateSessionToken := session.MockCreateToken(mockToken, mockCreatedAt, nil)
mockUserDTO := user.MockDTO()

login := auth.MakeLogin(mockSelectUserByUsername, mockCreateSessionToken)
logIn := auth.MakeLogIn(mockSelectUserByUsername, mockCreateSessionToken)

want := auth.FailedToSelectUserByUsername
_, _, got := login(context.Background(), mockUserDTO)
_, _, got := logIn(context.Background(), mockUserDTO)

assert.Equal(t, want, got)
}

func TestLogin_failsWhenCompareHashAndPasswordThrowsError(t *testing.T) {
func TestLogIn_failsWhenCompareHashAndPasswordThrowsError(t *testing.T) {
mockUserDAO := user.MockDAO()
mockSelectUserByUsername := user.MockSelectByUsername(mockUserDAO, nil)
mockCreatedAt := time.Date(2006, time.January, 1, 0, 0, 0, 0, time.Local)
mockCreateSessionToken := session.MockCreateToken("abcd", mockCreatedAt, nil)
mockUserDTO := user.MockDTO()
mockUserDTO.Password = "wrong password"

login := auth.MakeLogin(mockSelectUserByUsername, mockCreateSessionToken)
logIn := auth.MakeLogIn(mockSelectUserByUsername, mockCreateSessionToken)

want := auth.FailedToLoginDueWrongPassword
_, _, got := login(context.Background(), mockUserDTO)
_, _, got := logIn(context.Background(), mockUserDTO)

assert.Equal(t, want, got)
}

func TestLogin_failsWhenCreateSessionTokenThrowsError(t *testing.T) {
func TestLogIn_failsWhenCreateSessionTokenThrowsError(t *testing.T) {
mockUserDAO := user.MockDAO()
mockSelectUserByUsername := user.MockSelectByUsername(mockUserDAO, nil)
mockCreateSessionToken := session.MockCreateToken("abcd", time.Time{}, errors.New("error while executing CreateSessionToken"))
mockUserDTO := user.MockDTO()

login := auth.MakeLogin(mockSelectUserByUsername, mockCreateSessionToken)
logIn := auth.MakeLogIn(mockSelectUserByUsername, mockCreateSessionToken)

want := auth.FailedToCreateUserSession
_, _, got := login(context.Background(), mockUserDTO)
_, _, got := logIn(context.Background(), mockUserDTO)

assert.Equal(t, want, got)
}
8 changes: 8 additions & 0 deletions cmd/api/auth/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"context"
"time"

"ahbcc/cmd/api/user"
)
Expand All @@ -12,3 +13,10 @@ func MockSignUp(err error) SignUp {
return err
}
}

// MockLogIn mocks LogIn function
func MockLogIn(token string, expiresAt time.Time, err error) LogIn {
return func(ctx context.Context, user user.DTO) (string, time.Time, error) {
return token, expiresAt, err
}
}

0 comments on commit a76c695

Please sign in to comment.