package controllers import ( "fmt" "net/http" "net/http/httptest" "net/url" "path/filepath" "regexp" "strings" "testing" "time" "github.com/gin-gonic/gin" "GoMembership/internal/config" "GoMembership/internal/constants" "GoMembership/internal/models" "GoMembership/internal/utils" "GoMembership/pkg/logger" ) type RegisterUserTest struct { WantDBData map[string]interface{} Name string Input string WantResponse int Assert bool } 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 { return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, rt.WantResponse) } return nil } func (rt *RegisterUserTest) ValidateResult() error { return validateUser(rt.Assert, rt.WantDBData) } func TestUserController(t *testing.T) { tests := getTestUsers() for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { if err := runSingleTest(&tt); err != nil { t.Errorf("Test failed: %v", err.Error()) } }) } } 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 { //check for email delivery messages := utils.SMTPGetMessages() for _, message := range messages { mail, err := utils.DecodeMail(message.MsgRequest()) if err != nil { return err } if strings.Contains(mail.Subject, constants.MailRegistrationSubject) { if err := checkRegistrationMail(mail, &(*users)[0]); err != nil { return err } } else if strings.Contains(mail.Subject, constants.MailVerificationSubject) { if err := checkVerificationMail(mail, &(*users)[0]); err != nil { return err } verifiedUsers, err := Uc.Service.GetUsers(wantDBData) if err != nil { return err } if (*verifiedUsers)[0].Status != constants.VerifiedStatus { return fmt.Errorf("Users status isn't verified after email verification. Status is: %#v", (*verifiedUsers)[0].Status) } } else { return fmt.Errorf("Subject not expected: %v", mail.Subject) } } } return nil } 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.BaseURL) { return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.BaseURL) } 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.BaseURL) { return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.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.BaseURL) { return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.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("/backend/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 { return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", wv.Code, 200) } 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 getBaseUser() models.User { return models.User{ DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), FirstName: "John", LastName: "Doe", Email: "john.doe@example.com", Address: "Pablo Escobar Str. 4", ZipCode: "25474", City: "Hasloh", Phone: "01738484993", BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"}, Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}}, ProfilePicture: "", Password: "password123", Company: "", } } 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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.StatusNotAcceptable, WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, Assert: false, Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { user.BankAccount.IBAN = "" return user })), }, { Name: "Invalid IBAN should fail", WantResponse: http.StatusNotAcceptable, WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, Assert: false, Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { user.BankAccount.IBAN = "DE1234234123134" return user })), }, { Name: "Missing subscription plan should fail", WantResponse: http.StatusNotAcceptable, 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.StatusNotFound, 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.StatusNotAcceptable, 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.StatusNotAcceptable, 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 })), }, } }