Files
GoMembership/go-backend/internal/controllers/user_controller_test.go
2025-04-10 15:40:22 +02:00

1315 lines
46 KiB
Go

package controllers
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/internal/database"
"GoMembership/internal/middlewares"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
"github.com/golang-jwt/jwt/v5"
)
type RegisterUserTest struct {
WantDBData map[string]interface{}
Name string
Input string
WantResponse int
Assert bool
}
var jwtSigningMethod = jwt.SigningMethodHS256
func (rt *RegisterUserTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
return GetMockedJSONContext([]byte(rt.Input), "register")
}
func (rt *RegisterUserTest) RunHandler(c *gin.Context, router *gin.Engine) {
Uc.RegisterUser(c)
}
func (rt *RegisterUserTest) ValidateResponse(w *httptest.ResponseRecorder) error {
if w.Code != rt.WantResponse {
responseBody, _ := io.ReadAll(w.Body)
return fmt.Errorf("Register User: Didn't get the expected response code: got: %v; expected: %v. Context: %#v", w.Code, rt.WantResponse, string(responseBody))
}
return nil
}
func (rt *RegisterUserTest) ValidateResult() error {
return validateUser(rt.Assert, rt.WantDBData)
}
func testUserController(t *testing.T) {
tests := getTestUsers()
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Register User Testing : %v", tt.Name)
logger.Error.Print("==============================================================")
t.Run(tt.Name, func(t *testing.T) {
if err := runSingleTest(&tt); err != nil {
t.Fatalf("Test failed: %v", err.Error())
}
})
}
// activate user for login
database.DB.Model(&models.User{}).Where("email = ?", "john.doe@example.com").Update("status", constants.ActiveStatus)
loginEmail := testLoginHandler(t)
testCurrentUserHandler(t, loginEmail)
// creating a admin cookie
c, w, _ := GetMockedJSONContext([]byte(`{
"email": "admin@example.com",
"password": "securepassword"
}`), "/login")
Uc.LoginHandler(c)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Login successful", response["message"])
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
AdminCookie = cookie
tokenString := AdminCookie.Value
_, claims, err := middlewares.ExtractContentFrom(tokenString)
assert.NoError(t, err, "Failed getting cookie string")
jwtUserID := uint((*claims)["user_id"].(float64))
user, err := Uc.Service.FromID(&jwtUserID)
assert.NoError(t, err, "Failed getting cookie string")
logger.Error.Printf("ADMIN USER: %#v", user)
break
}
}
assert.NotEmpty(t, AdminCookie)
testUpdateUser(t)
testLogoutHandler(t)
testCreatePasswordHandler(t)
}
func testLogoutHandler(t *testing.T) {
tests := []struct {
name string
setupCookie func(*http.Request)
expectedStatus int
}{
{
name: "Logout with valid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
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 {
logger.Error.Print("==============================================================")
logger.Error.Printf("Logout User Testing : %v", tt.name)
logger.Error.Print("==============================================================")
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", nil)
if logoutCookie != nil {
req.AddCookie(logoutCookie)
}
router.GET("/current", 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 {
// This test should run after the user registration test
var loginInput loginInput
t.Run("LoginHandler", func(t *testing.T) {
// Test cases
tests := []struct {
name string
input string
wantStatusCode int
wantToken bool
}{
{
name: "Valid login",
input: `{
"email": "john.doe@example.com",
"password": "passw@#$#%$!-ord123"
}`,
wantStatusCode: http.StatusOK,
wantToken: true,
},
{
name: "Invalid email",
input: `{
"email": "nonexistent@example.com",
"password": "passw@#$#%$!-ord123"
}`,
wantStatusCode: http.StatusNotFound,
wantToken: false,
},
{
name: "Invalid password",
input: `{
"email": "john.doe@example.com",
"password": "wrongpassword"
}`,
wantStatusCode: http.StatusNotAcceptable,
wantToken: false,
},
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Login Testing : %v", tt.name)
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
// Setup
c, w, _ := GetMockedJSONContext([]byte(tt.input), "/login")
// Execute
Uc.LoginHandler(c)
// Assert
assert.Equal(t, tt.wantStatusCode, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
if tt.wantToken {
assert.Contains(t, response, "message")
assert.Equal(t, "Login successful", response["message"])
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
MemberCookie = cookie
tokenString := cookie.Value
_, claims, err := middlewares.ExtractContentFrom(tokenString)
assert.NoError(t, err, "FAiled getting cookie string")
jwtUserID := uint((*claims)["user_id"].(float64))
_, err = Uc.Service.FromID(&jwtUserID)
assert.NoError(t, err, "FAiled getting cookie string")
// logger.Error.Printf("cookie user: %#v", user)
err = json.Unmarshal([]byte(tt.input), &loginInput)
assert.NoError(t, err, "Failed to unmarshal input JSON")
break
}
}
assert.NotEmpty(t, MemberCookie)
} else {
assert.Contains(t, response, "errors")
assert.NotEmpty(t, response["errors"])
}
})
}
})
return loginInput.Email
}
func testCurrentUserHandler(t *testing.T, loginEmail string) http.Cookie {
// This test should run after the user login test
invalidCookie := http.Cookie{
Name: "jwt",
Value: "invalid.token.here",
}
tests := []struct {
name string
setupCookie func(*http.Request)
expectedUserMail string
expectedStatus int
expectNewCookie bool
expectedErrors []map[string]string
}{
{
name: "With valid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
expectedUserMail: loginEmail,
expectedStatus: http.StatusOK,
},
{
name: "With valid expired cookie",
setupCookie: func(req *http.Request) {
sessionID := "test-session"
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": 1,
"role_id": 0,
"session_id": sessionID,
"exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago
})
tokenString, _ := token.SignedString([]byte(config.Auth.JWTSecret))
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
middlewares.UpdateSession(sessionID, 1) // Add a valid session
},
expectedUserMail: config.Recipients.AdminEmail,
expectedStatus: http.StatusOK,
expectNewCookie: true,
},
{
name: "Without cookie",
setupCookie: func(req *http.Request) {},
expectedStatus: http.StatusUnauthorized,
expectedErrors: []map[string]string{
{"field": "server.general", "key": "server.error.no_auth_token"},
},
},
{
name: "With invalid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&invalidCookie)
},
expectedStatus: http.StatusUnauthorized,
expectedErrors: []map[string]string{
{"field": "server.general", "key": "server.error.no_auth_token"},
},
},
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("CurrentUser Testing : %v", tt.name)
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(middlewares.AuthMiddleware())
router.GET("/current", Uc.CurrentUserHandler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/current", nil)
tt.setupCookie(req)
router.ServeHTTP(w, req)
assert.Equal(t, tt.expectedStatus, w.Code)
if tt.expectedStatus == http.StatusOK {
var response struct {
User models.User `json:"user"`
Subscriptions []models.Subscription `json:"subscriptions"`
}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// logger.Error.Printf("response: %#v", response)
assert.Equal(t, tt.expectedUserMail, response.User.Email)
var newCookie *http.Cookie
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
newCookie = cookie
break
}
}
if tt.expectNewCookie {
assert.NotNil(t, newCookie, "New cookie should be set for expired token")
assert.NotEqual(t, MemberCookie.Value, newCookie.Value, "Cookie value should be different")
assert.True(t, newCookie.MaxAge > 0, "New cookie should not be expired")
} else {
assert.Nil(t, newCookie, "No new cookie should be set for non-expired token")
}
} else {
// For unauthorized requests, check for the new error structure
var errorResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
assert.NoError(t, err)
errors, ok := errorResponse["errors"].([]interface{})
assert.True(t, ok, "Expected 'errors' field in response")
assert.Len(t, errors, len(tt.expectedErrors), "Unexpected number of errors")
for i, expectedError := range tt.expectedErrors {
if i < len(errors) {
actualError := errors[i].(map[string]interface{})
assert.Equal(t, expectedError["field"], actualError["field"], "Mismatched error field")
assert.Equal(t, expectedError["key"], actualError["key"], "Mismatched error key")
}
}
}
})
}
return *MemberCookie
}
func validateUser(assert bool, wantDBData map[string]interface{}) error {
users, err := Uc.Service.GetUsers(wantDBData)
if err != nil {
return fmt.Errorf("Error in database ops: %#v", err)
}
if assert != (len(*users) != 0) {
return fmt.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users)
}
if assert {
user := (*users)[0]
// Check for mandate reference
if user.BankAccount.IBAN != "" && user.BankAccount.MandateReference == "" {
return fmt.Errorf("Mandate reference not generated for user: %s", user.Email)
} else if user.BankAccount.IBAN == "" && user.BankAccount.MandateReference != "" {
return fmt.Errorf("Mandate reference generated without IBAN for user: %s", user.Email)
}
// Validate mandate reference format
expected := user.BankAccount.GenerateMandateReference(user.ID)
if !strings.HasPrefix(user.BankAccount.MandateReference, expected) {
return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference)
}
// Supoorter don't get mails
if user.IsSupporter() {
return nil
}
//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.MailRegistrationSubject) {
if err := checkRegistrationMail(mail, &user); err != nil {
logger.Error.Printf("Error in checkRegistrationMail: %#v", err)
return err
}
} else if strings.Contains(mail.Subject, constants.MailVerificationSubject) {
if err := checkVerificationMail(mail, &user); err != nil {
logger.Error.Printf("Error in checkVerificationMail: %#v", err)
return err
}
verifiedUsers, err := Uc.Service.GetUsers(wantDBData)
if err != nil {
logger.Error.Printf("Error in GetUsers: %#v", err)
return err
}
if (*verifiedUsers)[0].Status != constants.VerifiedStatus {
return fmt.Errorf("Users(%v) status isn't verified after email verification. Status is: %v", (*verifiedUsers)[0].Email, (*verifiedUsers)[0].Status)
}
} else {
return fmt.Errorf("Subject not expected: %v", mail.Subject)
}
}
}
return nil
}
func testUpdateUser(t *testing.T) {
invalidCookie := http.Cookie{
Name: "jwt",
Value: "invalid.token.here",
}
// Get the user we just created
johnsMail := "john.doe@example.com"
user, err := Uc.Service.FromEmail(&johnsMail)
if err != nil {
t.Fatalf("Failed to get test user: %v", err)
}
if user.Licence == nil {
user.Licence = &models.Licence{
Number: "Z021AB37X13",
ExpirationDate: time.Now().UTC().AddDate(1, 0, 0),
IssuedDate: time.Now().UTC().AddDate(-1, 0, 0),
IssuingCountry: "Deutschland",
}
}
tests := []struct {
name string
setupCookie func(*http.Request)
updateFunc func(*models.User)
expectedReturn func(*models.User)
expectedStatus int
expectedErrors []map[string]string
}{
{
name: "Valid Admin Update",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
},
expectedStatus: http.StatusAccepted,
},
{
name: "Valid Update, invalid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&invalidCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
},
expectedStatus: http.StatusUnauthorized,
expectedErrors: []map[string]string{
{"field": "server.general", "key": "server.error.no_auth_token"},
},
},
{
name: "Invalid Email Update",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Email = "invalid-email"
},
expectedStatus: http.StatusBadRequest,
expectedErrors: []map[string]string{
{"field": "Email", "key": "server.validation.email"},
},
},
{
name: "admin may change licence number",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
},
expectedStatus: http.StatusAccepted,
},
{
name: "Change phone number",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
},
expectedStatus: http.StatusAccepted,
},
{
name: "Add category",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("B")
assert.NoError(t, err)
u.Licence.Categories = []*models.Category{&category}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete 1 and add 1 category",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("A")
category2, err := licenceRepo.FindCategoryByName("BE")
assert.NoError(t, err)
u.Licence.Categories = []*models.Category{&category, &category2}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete 1 category",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("A")
assert.NoError(t, err)
u.Licence.Categories = []*models.Category{&category}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete all categories",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.Licence.Categories = []*models.Category{}
},
expectedStatus: http.StatusAccepted,
},
{
name: "User ID mismatch while not admin",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.ID = 1
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.FirstName = "John Missing ID"
},
expectedStatus: http.StatusForbidden,
expectedErrors: []map[string]string{
{"field": "user.user", "key": "server.error.unauthorized"},
},
},
{
name: "Password Update low entropy should fail",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.Password = "newpassword"
},
expectedErrors: []map[string]string{
{"field": "server.validation.special server.validation.uppercase server.validation.numbers server.validation.longer", "key": "server.validation.insecure"},
},
expectedStatus: http.StatusBadRequest,
},
{
name: "Password Update",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.Password = "NewPa0293409@#-!ssword"
},
expectedReturn: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
},
expectedStatus: http.StatusAccepted,
},
{
name: "Admin Password Update low entropy should fail",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = "newpassword"
},
expectedErrors: []map[string]string{
{"field": "server.validation.special server.validation.uppercase server.validation.numbers server.validation.longer", "key": "server.validation.insecure"},
},
expectedStatus: http.StatusBadRequest,
},
{
name: "Admin Password Update",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.Password = "NewPassword"
},
expectedStatus: http.StatusAccepted,
},
{
name: "Non-existent User",
setupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.ID = 99999
u.FirstName = "Non-existent"
},
expectedErrors: []map[string]string{
{"field": "user.user", "key": "server.error.unauthorized"},
},
expectedStatus: http.StatusForbidden,
},
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Update Testing : %v", tt.name)
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
// Create a copy of the user and apply the updates
updatedUser := *user
// logger.Error.Printf("users licence to be updated: %+v", user.Licence)
tt.updateFunc(&updatedUser)
updateData := &RegistrationData{User: updatedUser}
jsonData, err := json.Marshal(updateData)
if err != nil {
t.Fatalf("Failed to marshal user data: %v", err)
}
logger.Error.Printf("Updated User: %#v", updatedUser.Safe())
if tt.expectedReturn != nil {
tt.expectedReturn(&updatedUser)
}
// Create request
req, _ := http.NewRequest("PUT", "/users/"+strconv.FormatUint(uint64(user.ID), 10), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
tt.setupCookie(req)
// Create response recorder
w := httptest.NewRecorder()
// Set up router and add middleware
router := gin.New()
router.Use(middlewares.AuthMiddleware())
router.PUT("/users/:id", Uc.UpdateHandler)
// Perform request
router.ServeHTTP(w, req)
bodyBytes, _ := io.ReadAll(w.Body)
t.Logf("Response Body: %s", string(bodyBytes))
// Check status code
assert.Equal(t, tt.expectedStatus, w.Code)
// Parse response
var response map[string]interface{}
err = json.Unmarshal(bodyBytes, &response)
if err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
if tt.expectedErrors != nil {
errors, ok := response["errors"].([]interface{})
if !ok {
t.Fatalf("Expected 'errors' field in response, got: %v", response)
}
assert.Len(t, errors, len(tt.expectedErrors), "Unexpected number of errors")
for i, expectedError := range tt.expectedErrors {
if i < len(errors) {
actualError := errors[i].(map[string]interface{})
assert.Equal(t, expectedError["field"], actualError["field"], "Mismatched error field")
assert.Equal(t, expectedError["key"], actualError["key"], "Mismatched error key")
}
}
} else {
// Check for success message
message, ok := response["message"].(string)
if !ok {
t.Fatalf("Expected 'message' field in response, got: %v", response)
}
assert.Equal(t, "User updated successfully", message)
// Verify the update in the database
updatedUserFromDB, err := Uc.Service.FromID(&user.ID)
assert.NoError(t, err)
if updatedUser.Password == "" {
assert.Equal(t, user.Password, (*updatedUserFromDB).Password)
} else {
matches, err := updatedUserFromDB.PasswordMatches(updatedUser.Password)
if err != nil {
t.Fatalf("Error matching password: %v", err)
}
assert.True(t, matches, "Password mismatch")
}
updatedUserFromDB.Password = ""
updatedUser.Password = ""
assert.Equal(t, updatedUser.FirstName, updatedUserFromDB.FirstName, "FirstName mismatch")
assert.Equal(t, updatedUser.LastName, updatedUserFromDB.LastName, "LastName mismatch")
assert.Equal(t, updatedUser.Email, updatedUserFromDB.Email, "Email mismatch")
assert.Equal(t, updatedUser.DateOfBirth, updatedUserFromDB.DateOfBirth, "DateOfBirth mismatch")
assert.Equal(t, updatedUser.Company, updatedUserFromDB.Company, "Company mismatch")
assert.Equal(t, updatedUser.Phone, updatedUserFromDB.Phone, "Phone mismatch")
assert.Equal(t, updatedUser.Notes, updatedUserFromDB.Notes, "Notes mismatch")
assert.Equal(t, updatedUser.Address, updatedUserFromDB.Address, "Address mismatch")
assert.Equal(t, updatedUser.ZipCode, updatedUserFromDB.ZipCode, "ZipCode mismatch")
assert.Equal(t, updatedUser.City, updatedUserFromDB.City, "City mismatch")
assert.Equal(t, updatedUser.Status, updatedUserFromDB.Status, "Status mismatch")
assert.Equal(t, updatedUser.RoleID, updatedUserFromDB.RoleID, "RoleID mismatch")
// For nested structs, you might want to compare individual fields
assert.Equal(t, updatedUser.BankAccount.Bank, updatedUserFromDB.BankAccount.Bank, "BankAccount.Bank mismatch")
assert.Equal(t, updatedUser.BankAccount.AccountHolderName, updatedUserFromDB.BankAccount.AccountHolderName, "BankAccount.AccountHolderName mismatch")
assert.Equal(t, updatedUser.BankAccount.IBAN, updatedUserFromDB.BankAccount.IBAN, "BankAccount.IBAN mismatch")
assert.Equal(t, updatedUser.BankAccount.BIC, updatedUserFromDB.BankAccount.BIC, "BankAccount.BIC mismatch")
assert.Equal(t, updatedUser.BankAccount.MandateReference, updatedUserFromDB.BankAccount.MandateReference, "BankAccount.MandateReference mismatch")
assert.Equal(t, updatedUser.Membership.StartDate, updatedUserFromDB.Membership.StartDate, "Membership.StartDate mismatch")
assert.Equal(t, updatedUser.Membership.EndDate, updatedUserFromDB.Membership.EndDate, "Membership.EndDate mismatch")
assert.Equal(t, updatedUser.Membership.Status, updatedUserFromDB.Membership.Status, "Membership.Status mismatch")
assert.Equal(t, updatedUser.Membership.SubscriptionID, updatedUserFromDB.Membership.SubscriptionID, "Membership.SubscriptionID mismatch")
assert.Equal(t, updatedUser.Membership.ParentMembershipID, updatedUserFromDB.Membership.ParentMembershipID, "Membership.ParentMembershipID mismatch")
if updatedUser.Licence == nil {
assert.Nil(t, updatedUserFromDB.Licence, "database licence of user is not nil, but user.licence is nil")
} else {
logger.Error.Printf("updatedUser licence: %#v", updatedUser.Licence)
logger.Error.Printf("dbUser licence: %#v", updatedUserFromDB.Licence)
assert.Equal(t, updatedUser.Licence.Status, updatedUserFromDB.Licence.Status, "Licence.Status mismatch")
assert.Equal(t, updatedUser.Licence.Number, updatedUserFromDB.Licence.Number, "Licence.Number mismatch")
assert.Equal(t, updatedUser.Licence.IssuedDate, updatedUserFromDB.Licence.IssuedDate, "Licence.IssuedDate mismatch")
assert.Equal(t, updatedUser.Licence.ExpirationDate, updatedUserFromDB.Licence.ExpirationDate, "Licence.ExpirationDate mismatch")
assert.Equal(t, updatedUser.Licence.IssuingCountry, updatedUserFromDB.Licence.IssuingCountry, "Licence.IssuingCountry mismatch")
}
if len(updatedUser.Consents) > 0 {
for i := range updatedUser.Consents {
assert.Equal(t, updatedUser.Consents[i].ConsentType, updatedUserFromDB.Consents[i].ConsentType, "ConsentType mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].Email, updatedUserFromDB.Consents[i].Email, "ConsentEmail mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].FirstName, updatedUserFromDB.Consents[i].FirstName, "ConsentFirstName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].LastName, updatedUserFromDB.Consents[i].LastName, "ConsentLastName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].UserID, updatedUserFromDB.Consents[i].UserID, "Consent UserId mismatch at index %d", i)
}
} else {
assert.Emptyf(t, updatedUserFromDB.Licence.Categories, "Categories aren't empty when they should")
}
if len(updatedUser.Licence.Categories) > 0 {
for i := range updatedUser.Licence.Categories {
assert.Equal(t, updatedUser.Licence.Categories[i].Name, updatedUserFromDB.Licence.Categories[i].Name, "Category Category mismatch at index %d", i)
}
} else {
assert.Emptyf(t, updatedUserFromDB.Licence.Categories, "Categories aren't empty when they should")
}
}
})
}
}
func checkWelcomeMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.To, user.Email) {
return fmt.Errorf("Registration Information didn't reach the user! Recipient was: %v instead of %v", message.To, user.Email)
}
if !strings.Contains(message.From, config.SMTP.User) {
return fmt.Errorf("Registration 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) {
return fmt.Errorf("User first name(%v) has not been rendered in registration mail.", user.FirstName)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
}
if !strings.Contains(message.Body, fmt.Sprintf("Mitgliedsnummer</strong>: %v", user.Membership.ID)) {
return fmt.Errorf("Users membership Id(%v) has not been rendered in registration mail.", user.Membership.ID)
}
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.Site.BaseURL)
}
if !strings.Contains(message.Body, config.Site.BaseURL+config.Templates.LogoURI) {
return fmt.Errorf("Logo Url (%v) has not been rendered in registration mail.", config.Site.BaseURL+config.Site.WebsiteTitle)
}
if !strings.Contains(message.Body, config.Site.WebsiteTitle) {
return fmt.Errorf("Website title (%v) has not been rendered in registration mail.", config.Site.WebsiteTitle)
}
return nil
}
func checkRegistrationMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.To, config.Recipients.UserRegistration) {
return fmt.Errorf("Registration Information didn't reach admin! Recipient was: %v instead of %v", message.To, config.Recipients.UserRegistration)
}
if !strings.Contains(message.From, config.SMTP.User) {
return fmt.Errorf("Registration 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 registration mail.", user.FirstName+" "+user.LastName)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
}
if !strings.Contains(message.Body, fmt.Sprintf("Mitgliedsnr:</strong> %v", user.Membership.ID)) {
return fmt.Errorf("Users membership Id(%v) has not been rendered in registration mail.", user.Membership.ID)
}
if !strings.Contains(message.Body, user.Address+","+user.ZipCode) {
return fmt.Errorf("Users address(%v) has not been rendered in registration mail.", user.Address+" sv,"+user.ZipCode)
}
if !strings.Contains(message.Body, user.City) {
return fmt.Errorf("Users city(%v) has not been rendered in registration mail.", user.City)
}
if !strings.Contains(message.Body, user.DateOfBirth.Format("20060102")) {
return fmt.Errorf("Users birthday(%v) has not been rendered in registration mail.", user.DateOfBirth.Format("20060102"))
}
if !strings.Contains(message.Body, "Email:</strong> "+user.Email) {
return fmt.Errorf("Users email(%v) has not been rendered in registration mail.", user.Email)
}
if !strings.Contains(message.Body, user.Phone) {
return fmt.Errorf("Users phone(%v) has not been rendered in registration mail.", user.Phone)
}
if !strings.Contains(message.Body, user.BankAccount.IBAN) {
return fmt.Errorf("Users IBAN(%v) has not been rendered in registration mail.", user.BankAccount.IBAN)
}
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.Site.BaseURL)
}
return nil
}
func checkVerificationMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.To, user.Email) {
return fmt.Errorf("Registration Information didn't reach client! Recipient was: %v instead of %v", message.To, user.Email)
}
verificationURL, err := getVerificationURL(message.Body)
if err != nil {
return fmt.Errorf("Error parsing verification URL: %#v", err.Error())
}
v, err := user.FindVerification(constants.VerificationTypes.Email)
if err != nil {
return fmt.Errorf("Error getting verification token: %v", err.Error())
}
if !strings.Contains(verificationURL, v.VerificationToken) {
return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", v.VerificationToken, verificationURL)
}
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.Site.BaseURL)
}
// open the provided link:
if err := verifyMail(verificationURL, user.ID); err != nil {
return err
}
messages := utils.SMTPGetMessages()
for _, message := range messages {
mail, err := utils.DecodeMail(message.MsgRequest())
if err != nil {
return err
}
if err := checkWelcomeMail(mail, user); err != nil {
return err
}
}
return nil
}
func verifyMail(verificationURL string, user_id uint) error {
gin.SetMode(gin.TestMode)
router := gin.New()
router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*"))
expectedUrl := fmt.Sprintf("/api/users/verify/%v", user_id)
log.Printf("Expected URL: %v", expectedUrl)
router.GET("/api/users/verify/:id", Uc.VerifyMailHandler)
wv := httptest.NewRecorder()
cv, _ := gin.CreateTestContext(wv)
var err error
cv.Request, err = http.NewRequest("GET", verificationURL, nil)
if err != nil {
return fmt.Errorf("Failed to create new GET Request: %v", err.Error())
}
router.ServeHTTP(wv, cv.Request)
if wv.Code != 200 {
responseBody, _ := io.ReadAll(wv.Body)
return fmt.Errorf("VerifyMail: Didn't get the expected response code: got: %v; expected: %v Context: %#v", wv.Code, 200, string(responseBody))
}
return nil
}
func getVerificationURL(mailBody string) (string, error) {
re := regexp.MustCompile(`"([^"]*verify[^"]*)"`)
// Find the matching URL in the email content
match := re.FindStringSubmatch(mailBody)
if len(match) == 0 {
return "", fmt.Errorf("No mail verification link found in email body: %#v", mailBody)
}
verificationURL, err := url.QueryUnescape(match[1])
if err != nil {
return "", fmt.Errorf("Error decoding URL: %v", err)
}
logger.Info.Printf("VerificationURL: %#v", verificationURL)
return verificationURL, nil
}
// TEST DATA:
func customizeInput(customize func(models.User) models.User) *RegistrationData {
user := getBaseUser()
user = customize(user) // Apply the customization
return &RegistrationData{User: user}
}
func getTestUsers() []RegisterUserTest {
return []RegisterUserTest{
{
Name: "birthday < 18 should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.DateOfBirth = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
return user
})),
},
{
Name: "FirstName empty, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.FirstName = ""
return user
})),
},
{
Name: "LastName Empty should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.LastName = ""
return user
})),
},
{
Name: "EMail wrong format should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "johnexample.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "johnexample.com"
return user
})),
},
{
Name: "Missing Zip Code should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.ZipCode = ""
return user
})),
},
{
Name: "Missing Address should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Address = ""
return user
})),
},
{
Name: "Missing City should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.City = ""
return user
})),
},
{
Name: "Missing IBAN should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = ""
user.RoleID = 1
return user
})),
},
{
Name: "Invalid IBAN should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = "DE1234234123134"
user.RoleID = 1
return user
})),
},
{
Name: "invalid IBAN should fail when supporter",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.supporter@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = "DE1234234123134"
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
{
Name: "empty IBAN should pass when supporter",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.supporter@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = ""
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
{
Name: "Missing subscription plan should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.Subscription.Name = ""
return user
})),
},
{
Name: "Invalid subscription plan should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.Subscription.Name = "NOTEXISTENTPLAN"
return user
})),
},
{
Name: "Correct Entry should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { return user })),
},
{
Name: "Email duplicate should fail",
WantResponse: http.StatusConflict,
WantDBData: map[string]interface{}{"first_name": "Jane"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.FirstName = "Jane"
return user
})),
},
{
Name: "Company present should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.doe2@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.doe2@example.com"
user.Company = "ACME"
return user
})),
},
{
Name: "Subscription constraints not entered; should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.Subscription.Name = "additional"
return user
})),
},
{
Name: "Subscription constraints wrong; should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 200
user.Membership.Subscription.Name = "additional"
return user
})),
},
{
Name: "Subscription constraints correct, should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 1
user.Membership.Subscription.Name = "additional"
return user
})),
},
{
Name: "Correct Entry with Mandate Reference",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.mandate@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.mandate@example.com"
user.BankAccount.IBAN = "DE89370400440532013000"
return user
})),
},
// Currently unsupported. My number wouldn't match, though it should.
// {
// Name: "wrong driverslicence number, should fail",
// WantResponse: http.StatusBadRequest,
// WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
// Assert: false,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.wronglicence.doe@example.com"
// user.Licence = &models.Licence{
// Number: "AAAA12345AA",
// ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0),
// }
// return user
// })),
// },
{
Name: "empty driverslicence number, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.wronglicence.doe@example.com"
user.Licence = &models.Licence{
Number: "",
ExpirationDate: time.Now().AddDate(1, 0, 0),
IssuedDate: time.Now().AddDate(-1, 0, 0),
}
return user
})),
},
{
Name: "Correct Licence number, should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.correctlicencenumber@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.correctLicenceNumber@example.com"
user.Licence = &models.Licence{
Number: "B072RRE2I55",
ExpirationDate: time.Now().AddDate(1, 0, 0),
IssuedDate: time.Now().AddDate(-1, 0, 0),
}
return user
})),
},
}
}