diff --git a/internal/controllers/user_controller.go b/internal/controllers/user_controller.go index a79e1b1..0bd061b 100644 --- a/internal/controllers/user_controller.go +++ b/internal/controllers/user_controller.go @@ -43,7 +43,13 @@ func (uc *UserController) CurrentUserHandler(c *gin.Context) { c.JSON(http.StatusOK, user.Safe()) } -func (uc *UserController) LoginUser(c *gin.Context) { +func (uc *UserController) LogoutHandler(c *gin.Context) { + // just clear the JWT cookie + c.SetCookie("jwt", "", -1, "/", "", true, true) + c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) +} + +func (uc *UserController) LoginHandler(c *gin.Context) { var input struct { Email string `json:"email"` Password string `json:"password"` diff --git a/internal/controllers/user_controller_test.go b/internal/controllers/user_controller_test.go index b2be844..49d7b35 100644 --- a/internal/controllers/user_controller_test.go +++ b/internal/controllers/user_controller_test.go @@ -70,11 +70,77 @@ func testUserController(t *testing.T) { testCurrentUserHandler(t) } -func testLoginUser(t *testing.T) (string, http.Cookie) { +func testLogoutHandler(t *testing.T) { + loginCookie := testCurrentUserHandler(t) + + tests := []struct { + name string + setupCookie func(*http.Request) + expectedStatus int + }{ + { + name: "Logout with valid cookie", + setupCookie: func(req *http.Request) { + req.AddCookie(&loginCookie) + }, + expectedStatus: http.StatusOK, + }, + { + name: "Logout without cookie", + setupCookie: func(req *http.Request) {}, + expectedStatus: http.StatusOK, // Logout should still succeed even without a cookie + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + router.POST("/logout", Uc.LogoutHandler) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/logout", nil) + tt.setupCookie(req) + + router.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + var response map[string]string + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Logged out successfully", response["message"]) + + // Check if the cookie has been cleared + var logoutCookie *http.Cookie + for _, cookie := range w.Result().Cookies() { + if cookie.Name == "jwt" { + logoutCookie = cookie + break + } + } + assert.NotNil(t, logoutCookie, "Logout should set a clearing cookie") + assert.Equal(t, "", logoutCookie.Value, "Logout cookie should have empty value") + assert.True(t, logoutCookie.Expires.Before(time.Now()), "Logout cookie should be expired") + + // Verify that the user can no longer access protected routes + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/current-user", nil) + if logoutCookie != nil { + req.AddCookie(logoutCookie) + } + router.GET("/current-user", middlewares.AuthMiddleware(), Uc.CurrentUserHandler) + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusUnauthorized, w.Code, "User should not be able to access protected routes after logout") + }) + } +} + +func testLoginHandler(t *testing.T) (string, http.Cookie) { // This test should run after the user registration test var loginCookie http.Cookie var loginInput loginInput - t.Run("LoginUser", func(t *testing.T) { + t.Run("LoginHandler", func(t *testing.T) { // Test cases tests := []struct { name string @@ -120,7 +186,7 @@ func testLoginUser(t *testing.T) (string, http.Cookie) { c, w, _ := GetMockedJSONContext([]byte(tt.input), "/login") // Execute - Uc.LoginUser(c) + Uc.LoginHandler(c) // Assert assert.Equal(t, tt.wantStatusCode, w.Code) @@ -154,8 +220,8 @@ func testLoginUser(t *testing.T) (string, http.Cookie) { return loginInput.Email, loginCookie } -func testCurrentUserHandler(t *testing.T) { - loginEmail, loginCookie := testLoginUser(t) +func testCurrentUserHandler(t *testing.T) http.Cookie { + loginEmail, loginCookie := testLoginHandler(t) // This test should run after the user login test invalidCookie := http.Cookie{ Name: "jwt", @@ -238,6 +304,7 @@ func testCurrentUserHandler(t *testing.T) { }) } + return loginCookie } func validateUser(assert bool, wantDBData map[string]interface{}) error { @@ -460,7 +527,7 @@ func getBaseUser() models.User { func customizeInput(customize func(models.User) models.User) *RegistrationData { user := getBaseUser() user = customize(user) // Apply the customization - return &RegistrationData{User: user, Password: user.Password} + return &RegistrationData{User: user} } func getTestUsers() []RegisterUserTest { diff --git a/internal/routes/routes.go b/internal/routes/routes.go index d04262e..5e65e92 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -12,7 +12,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll router.POST("/users/register", userController.RegisterUser) router.POST("/users/contact", contactController.RelayContactRequest) - router.POST("/users/login", userController.LoginUser) + router.POST("/users/login", userController.LoginHandler) router.POST("/csp-report", middlewares.CSPReportHandling) // create subrouter for teh authenticated area /account @@ -30,5 +30,6 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll authRouter.Use(middlewares.AuthMiddleware()) { authRouter.POST("/currentUser", userController.CurrentUserHandler) + authRouter.POST("/logout", userController.LogoutHandler) } }