From a76c6953c8020a6b7a9414d873b451889c41f946 Mon Sep 17 00:00:00 2001 From: Lucas Belfanti Date: Mon, 13 Jan 2025 00:45:52 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Implemented=20/auth/login/?= =?UTF-8?q?v1=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/api/auth/dtos.go | 9 ++++ cmd/api/auth/handler.go | 63 ++++++++++++++++++++++---- cmd/api/auth/handler_test.go | 87 ++++++++++++++++++++++++++++++++++++ cmd/api/auth/login.go | 8 ++-- cmd/api/auth/login_test.go | 30 ++++++------- cmd/api/auth/mocks.go | 8 ++++ 6 files changed, 178 insertions(+), 27 deletions(-) create mode 100644 cmd/api/auth/dtos.go diff --git a/cmd/api/auth/dtos.go b/cmd/api/auth/dtos.go new file mode 100644 index 0000000..c106da3 --- /dev/null +++ b/cmd/api/auth/dtos.go @@ -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"` +} diff --git a/cmd/api/auth/handler.go b/cmd/api/auth/handler.go index 4114adb..5a540c7 100644 --- a/cmd/api/auth/handler.go +++ b/cmd/api/auth/handler.go @@ -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 == "" { diff --git a/cmd/api/auth/handler_test.go b/cmd/api/auth/handler_test.go index f19fa8f..ad79bdb 100644 --- a/cmd/api/auth/handler_test.go +++ b/cmd/api/auth/handler_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" @@ -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) + } + +} diff --git a/cmd/api/auth/login.go b/cmd/api/auth/login.go index 68ad81a..be1a461 100644 --- a/cmd/api/auth/login.go +++ b/cmd/api/auth/login.go @@ -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 { diff --git a/cmd/api/auth/login_test.go b/cmd/api/auth/login_test.go index 7ed52d5..14aec8e 100644 --- a/cmd/api/auth/login_test.go +++ b/cmd/api/auth/login_test.go @@ -13,24 +13,24 @@ 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" @@ -38,15 +38,15 @@ func TestLogin_failsWhenSelectUserByUsernameThrowsError(t *testing.T) { 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) @@ -54,24 +54,24 @@ func TestLogin_failsWhenCompareHashAndPasswordThrowsError(t *testing.T) { 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) } diff --git a/cmd/api/auth/mocks.go b/cmd/api/auth/mocks.go index c1a8f08..2513a72 100644 --- a/cmd/api/auth/mocks.go +++ b/cmd/api/auth/mocks.go @@ -2,6 +2,7 @@ package auth import ( "context" + "time" "ahbcc/cmd/api/user" ) @@ -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 + } +}