Files
GoMembership/go-backend/internal/controllers/user_Password_test.go
2025-03-24 17:46:11 +01:00

214 lines
7.5 KiB
Go

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) {
testEmail := "john.doe@example.com"
user, err := Uc.Service.FromEmail(&testEmail)
if err != nil {
logger.Error.Printf("error fetching user: %#v", err)
return nil, err
}
logger.Error.Printf("found user: %#v", user)
return &TestContext{
router: gin.Default(),
response: httptest.NewRecorder(),
user: user,
}, nil
}
func testCreatePasswordHandler(t *testing.T) {
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)
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(MemberCookie)
tc.router.ServeHTTP(tc.response, req)
logger.Error.Printf("Test results for %#v", t.Name())
assert.Equal(t, http.StatusUnauthorized, 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
}