package controllers import ( "bytes" "encoding/json" "fmt" "io" "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, loginCookie := testLoginHandler(t) logoutCookie := testCurrentUserHandler(t, loginEmail, loginCookie) // 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"]) var adminCookie http.Cookie 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.GetUserByID(jwtUserID) assert.NoError(t, err, "FAiled getting cookie string") logger.Error.Printf("ADMIN USER: %#v", user) break } } assert.NotEmpty(t, adminCookie) testUpdateUser(t, loginCookie, adminCookie) testLogoutHandler(t, logoutCookie) } func testLogoutHandler(t *testing.T, loginCookie http.Cookie) { 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 { 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, http.Cookie) { // This test should run after the user registration test var loginCookie http.Cookie 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": "password123" }`, wantStatusCode: http.StatusOK, wantToken: true, }, { name: "Invalid email", input: `{ "email": "nonexistent@example.com", "password": "password123" }`, 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" { loginCookie = *cookie // tokenString := loginCookie.Value // _, claims, err := middlewares.ExtractContentFrom(tokenString) // assert.NoError(t, err, "FAiled getting cookie string") // jwtUserID := uint((*claims)["user_id"].(float64)) // user, err := Uc.Service.GetUserByID(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, loginCookie) } else { assert.Contains(t, response, "errors") assert.NotEmpty(t, response["errors"]) } }) } }) return loginInput.Email, loginCookie } func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Cookie) 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(&loginCookie) }, 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": "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": "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.SubscriptionModel `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, loginCookie.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 loginCookie } 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.MandateReference == "" { return fmt.Errorf("Mandate reference not generated for user: %s", user.Email) } // Validate mandate reference format expected := user.GenerateMandateReference() if !strings.HasPrefix(user.BankAccount.MandateReference, expected) { return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference) } //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, loginCookie http.Cookie, adminCookie http.Cookie) { invalidCookie := http.Cookie{ Name: "jwt", Value: "invalid.token.here", } // Get the user we just created users, err := Uc.Service.GetUsers(map[string]interface{}{"email": "john.doe@example.com"}) if err != nil || len(*users) == 0 { t.Fatalf("Failed to get test user: %v", err) } user := (*users)[0] 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": "general", "key": "server.error.no_auth_token"}, }, }, { name: "Invalid Email Update", setupCookie: func(req *http.Request) { req.AddCookie(&loginCookie) }, 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(&loginCookie) }, 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(&loginCookie) }, 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(&loginCookie) }, 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(&loginCookie) }, 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(&loginCookie) }, 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(&loginCookie) }, 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.StatusUnauthorized, expectedErrors: []map[string]string{ {"field": "user.user", "key": "server.error.unauthorized"}, }, }, { name: "Password Update", setupCookie: func(req *http.Request) { req.AddCookie(&loginCookie) }, updateFunc: func(u *models.User) { u.FirstName = "John Updated" u.LastName = "Doe Updated" u.Phone = "01738484994" u.Licence.Number = "B072RRE2I50" u.Password = "NewPassword" }, 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", 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(&loginCookie) }, 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.StatusUnauthorized, }, } 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.GetUserByID(user.ID) assert.NoError(t, err) if updatedUser.Password == "" { assert.Equal(t, user.Password, (*updatedUserFromDB).Password) } else { assert.NotEqual(t, user.Password, (*updatedUserFromDB).Password) } 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.ProfilePicture, updatedUserFromDB.ProfilePicture, "ProfilePicture 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.PaymentStatus, updatedUserFromDB.PaymentStatus, "PaymentStatus 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.SubscriptionModelID, updatedUserFromDB.Membership.SubscriptionModelID, "Membership.SubscriptionModelID mismatch") assert.Equal(t, updatedUser.Membership.ParentMembershipID, updatedUserFromDB.Membership.ParentMembershipID, "Membership.ParentMembershipID mismatch") 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") // For slices or more complex nested structures, you might want to use deep equality checks assert.ElementsMatch(t, updatedUser.Consents, updatedUserFromDB.Consents, "Consents mismatch") 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: %v", user.Membership.SubscriptionModel.MonthlyFee)) { return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee) } if !strings.Contains(message.Body, fmt.Sprintf("Preis/h: %v", user.Membership.SubscriptionModel.HourlyRate)) { return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.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: %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: %v", user.Membership.SubscriptionModel.MonthlyFee)) { return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee) } if !strings.Contains(message.Body, fmt.Sprintf("Preis/h: %v", user.Membership.SubscriptionModel.HourlyRate)) { return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.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: %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: "+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()) } if !strings.Contains(verificationURL, user.Verification.VerificationToken) { return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", user.Verification.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); 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) error { gin.SetMode(gin.TestMode) router := gin.New() router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*")) router.GET("/users/verify", 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 = 0 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 = 0 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.SubscriptionModel.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.SubscriptionModel.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.SubscriptionModel.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.SubscriptionModel.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.SubscriptionModel.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 // })), // }, } }