package controllers import ( "GoMembership/internal/config" "GoMembership/internal/constants" "GoMembership/internal/database" "GoMembership/internal/models" "GoMembership/internal/utils" "GoMembership/pkg/logger" "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "regexp" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) type TestContext struct { router *gin.Engine response *httptest.ResponseRecorder user *models.User } func setupTestContext() (*TestContext, error) { user, err := Uc.Service.GetUserByEmail("john.doe@example.com") if err != nil { return nil, err } return &TestContext{ router: gin.Default(), response: httptest.NewRecorder(), user: user, }, nil } func testCreatePasswordHandler(t *testing.T, loginCookie http.Cookie, adminCookie http.Cookie) { invalidCookie := http.Cookie{ Name: "jwt", Value: "invalid.token.here", } tc, err := setupTestContext() if err != nil { t.Fatal(err) } tc.router.POST("/password", Uc.CreatePasswordHandler) requestBody := map[string]interface{}{ "user": map[string]interface{}{ "id": tc.user.ID, }, } body, _ := json.Marshal(requestBody) t.Run("successful password creation request from admin", func(t *testing.T) { req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body)) req.AddCookie(&adminCookie) tc.router.ServeHTTP(tc.response, req) logger.Error.Printf("Test results for %#v", t.Name()) assert.Equal(t, http.StatusAccepted, tc.response.Code) assert.JSONEq(t, `{"message":"password_change_requested"}`, tc.response.Body.String()) err = checkEmailDelivery(tc.user, true) assert.NoError(t, err) }) // test token and password change testChangePassword(t, tc) logger.Error.Printf("__________END RESULTS---------") tc.response = httptest.NewRecorder() t.Run("failed password creation request from member", func(t *testing.T) { req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body)) req.AddCookie(&loginCookie) tc.router.ServeHTTP(tc.response, req) logger.Error.Printf("Test results for %#v", t.Name()) assert.Equal(t, http.StatusForbidden, tc.response.Code) assert.JSONEq(t, `{"errors":[{"field":"user.user","key":"server.error.unauthorized"}]}`, tc.response.Body.String()) err = checkEmailDelivery(tc.user, false) assert.NoError(t, err) }) logger.Error.Printf("__________END RESULTS---------") tc.response = httptest.NewRecorder() t.Run("failed password creation request for invalid cookie", func(t *testing.T) { req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body)) req.AddCookie(&invalidCookie) tc.router.ServeHTTP(tc.response, req) logger.Error.Printf("Test results for %#v", t.Name()) assert.Equal(t, http.StatusBadRequest, tc.response.Code) assert.Contains(t, tc.response.Body.String(), `server.error.no_auth_token`) err = checkEmailDelivery(tc.user, false) assert.NoError(t, err) }) logger.Error.Printf("__________END RESULTS---------") tc.response = httptest.NewRecorder() } func testChangePassword(t *testing.T, tc *TestContext) { var verification models.Verification result := database.DB.Where("user_id = ? AND type = ?", tc.user.ID, constants.VerificationTypes.Password).First(&verification) assert.NoError(t, result.Error) logger.Error.Printf("token from db: %#v", verification.VerificationToken) requestBody := map[string]interface{}{ "password": "new-pas9247A@!sword", "token": verification.VerificationToken, } body, _ := json.Marshal(requestBody) tc.router.PUT("/users/:id/password", Uc.ChangePassword) tc.response = httptest.NewRecorder() t.Run("valid password change", func(t *testing.T) { req, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%v/password", tc.user.ID), bytes.NewBuffer(body)) tc.router.ServeHTTP(tc.response, req) assert.Equal(t, http.StatusOK, tc.response.Code) assert.JSONEq(t, `{"message":"password_changed"}`, tc.response.Body.String()) }) tc.response = httptest.NewRecorder() //User should now be deactivated. Should lack privileges. t.Run("user lacks privileges", func(t *testing.T) { req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body)) tc.router.ServeHTTP(tc.response, req) logger.Error.Printf("Test results for %#v", t.Name()) assert.Equal(t, http.StatusBadRequest, tc.response.Code) assert.Contains(t, tc.response.Body.String(), `server.error.no_auth_token`) }) logger.Error.Printf("__________END RESULTS---------") t.Run("invalid user ID", func(t *testing.T) { req, _ := http.NewRequest("PUT", "/users/invalid/password", nil) tc.router.ServeHTTP(tc.response, req) assert.Equal(t, http.StatusBadRequest, tc.response.Code) }) t.Run("non existant user ID", func(t *testing.T) { req, _ := http.NewRequest("PUT", "/users/999/password", nil) tc.router.ServeHTTP(tc.response, req) assert.Equal(t, http.StatusBadRequest, tc.response.Code) }) } func checkEmailDelivery(user *models.User, wantsSuccess bool) error { //check for email delivery messages := utils.SMTPGetMessages() for _, message := range messages { mail, err := utils.DecodeMail(message.MsgRequest()) if err != nil { logger.Error.Printf("Error in validateUser: %#v", err) return err } if strings.Contains(mail.Subject, constants.MailChangePasswordSubject) || strings.Contains(mail.Subject, constants.MailGrantBackendAccessSubject) { if err := checkPasswordMail(mail, user); err != nil && wantsSuccess { logger.Error.Printf("Error in checkEmailDelivery mail: %#v", err) return err } } else { return fmt.Errorf("Subject not expected: %v", mail.Subject) } } return nil } func checkPasswordMail(message *utils.Email, user *models.User) error { var verification models.Verification result := database.DB.Where("user_id = ? AND type = ?", user.ID, constants.VerificationTypes.Password).First(&verification) if result.Error != nil { return result.Error } logger.Error.Printf("user id: %v token: %#v", user.ID, verification.VerificationToken) re := regexp.MustCompile(`"([^"]*token[^"]*)"`) // Find the matching URL in the email content match := re.FindStringSubmatch(message.Body) if len(match) == 0 { return fmt.Errorf("No change Password link found in email body: %#v", message.Body) } tokenURL, err := url.QueryUnescape(match[1]) if err != nil { return fmt.Errorf("Error decoding URL: %v", err) } logger.Info.Printf("TokenURL: %#v", tokenURL) if !strings.Contains(message.To, user.Email) { return fmt.Errorf("Password Information didn't reach user! Recipient was: %v instead of %v", message.To, user.Email) } if !strings.Contains(message.From, config.SMTP.User) { return fmt.Errorf("Password Information was sent from unexpected address! Sender was: %v instead of %v", message.From, config.SMTP.User) } //Check if all the relevant data has been passed to the mail. if !strings.Contains(message.Body, user.FirstName+" "+user.LastName) { return fmt.Errorf("User first and last name(%v) has not been rendered in password mail.", user.FirstName+" "+user.LastName) } if !strings.Contains(message.Body, verification.VerificationToken) { return fmt.Errorf("Token(%v) has not been rendered in password mail.", verification.VerificationToken) } if strings.Trim(tokenURL, " ") != fmt.Sprintf("%v%v/auth/password/change/%v?token=%v", config.Site.BaseURL, config.Site.FrontendPath, user.ID, verification.VerificationToken) { return fmt.Errorf("Token has not been rendered correctly in password mail: %v%v/auth/password/change/%v?token=%v", config.Site.BaseURL, config.Site.FrontendPath, user.ID, verification.VerificationToken) } return nil }