package controllers import ( "bytes" "encoding/json" "fmt" "strconv" "strings" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/gin-gonic/gin" smtpmock "github.com/mocktools/go-smtp-mock/v2" "GoMembership/internal/config" "GoMembership/internal/constants" "GoMembership/internal/database" "GoMembership/internal/models" "GoMembership/internal/repositories" "GoMembership/internal/services" "GoMembership/internal/utils" "GoMembership/pkg/logger" ) type test struct { name string input string wantDBData map[string]interface{} wantResponse uint16 assert bool } // type RegistrationData struct { // User models.User `json:"user"` // } const ( Host = "127.0.0.1" Port int = 2525 User = "alex@mail.de" Pass = "secret" AdminMail = "admin@mail.de" ) var ( uc UserController ) func TestUserController(t *testing.T) { _ = deleteTestDB("test.db") if err := database.InitDB("test.db"); err != nil { t.Errorf("Failed to create DB: %#v", err) } if err := os.Setenv("SMTP_HOST", Host); err != nil { t.Errorf("Error setting environment variable: %v", err) } if err := os.Setenv("SMTP_PORT", strconv.Itoa(Port)); err != nil { t.Errorf("Error setting environment variable: %v", err) } if err := os.Setenv("ADMIN_MAIL", AdminMail); err != nil { t.Errorf("Error setting environment variable: %v", err) } if err := os.Setenv("SMTP_USER", User); err != nil { t.Errorf("Error setting environment variable: %v", err) } if err := os.Setenv("SMTP_PASS", Pass); err != nil { t.Errorf("Error setting environment variable: %v", err) } config.LoadConfig() utils.SMTPStart(Host, Port) emailService := services.NewEmailService(config.SMTP.Host, config.SMTP.Port, config.SMTP.User, config.SMTP.Password, config.SMTP.AdminEmail) var consentRepo repositories.ConsentRepositoryInterface = &repositories.ConsentRepository{} consentService := &services.ConsentService{Repo: consentRepo} var bankAccountRepo repositories.BankAccountRepositoryInterface = &repositories.BankAccountRepository{} bankAccountService := &services.BankAccountService{Repo: bankAccountRepo} var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{} var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{} membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo} var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{} userService := &services.UserService{Repo: userRepo} uc = UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService} if err := initSubscriptionPlans(); err != nil { t.Errorf("Failed to init Susbcription plans: %#v", err) } 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()) } }) } if err := utils.SMTPStop(); err != nil { t.Errorf("Failed to stop SMTP Mockup Server: %#v", err) } if err := deleteTestDB("test.db"); err != nil { t.Errorf("Failed to tear down DB: %#v", err) } } func runSingleTest(tt *test) error { c, w := getMockedContext([]byte(tt.input)) uc.RegisterUser(c) if w.Code != int(tt.wantResponse) { return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, tt.wantResponse) } return validateUser(tt.assert, tt.wantDBData) } 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 { if strings.Contains(message.MsgRequest(), constants.MailWelcomeSubject) { if err := checkWelcomeMail(&message); err != nil { return err } } else if strings.Contains(message.MsgRequest(), constants.MailRegistrationSubject) { if err := checkRegistrationMail(&message, &(*users)[0]); err != nil { return err } } else if strings.Contains(message.MsgRequest(), constants.MailVerificationSubject) { if err := checkVerificationMail(&message, &(*users)[0]); err != nil { return err } } else { return fmt.Errorf("Subject not expected: %v", message.MsgRequest()) } } } return nil } func checkWelcomeMail(message *smtpmock.Message) error { return nil } func checkRegistrationMail(message *smtpmock.Message, user *models.User) error { for _, responses := range message.RcpttoRequestResponse() { if !strings.Contains(responses[0], AdminMail) { return fmt.Errorf("Registration Information didn't reach admin! Recipient was: %v instead of %v", responses[0], AdminMail) } } if !strings.Contains(message.MailfromRequest(), User) { return fmt.Errorf("Registration Information was sent from unexpected address! Sender was: %v instead of %v", message.MailfromRequest(), User) } //Check if all the relevant data has been passed to the mail. if !strings.Contains(message.MsgRequest(), 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.MsgRequest(), 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.MsgRequest(), 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.MsgRequest(), user.Company) { return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company) } if !strings.Contains(message.MsgRequest(), 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.MsgRequest(), user.Address+","+user.ZipCode) { return fmt.Errorf("Users address(%v) has not been rendered in registration mail.", user.Address+","+user.ZipCode) } if !strings.Contains(message.MsgRequest(), user.City) { return fmt.Errorf("Users city(%v) has not been rendered in registration mail.", user.City) } if !strings.Contains(message.MsgRequest(), 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.MsgRequest(), "Email: "+user.Email) { return fmt.Errorf("Users email(%v) has not been rendered in registration mail.", user.Email) } if !strings.Contains(message.MsgRequest(), user.Phone) { return fmt.Errorf("Users phone(%v) has not been rendered in registration mail.", user.Phone) } if !strings.Contains(message.MsgRequest(), user.BankAccount.IBAN) { return fmt.Errorf("Users IBAN(%v) has not been rendered in registration mail.", user.BankAccount.IBAN) } if !strings.Contains(message.MsgRequest(), config.BaseURL) { return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.BaseURL) } return nil } func checkVerificationMail(message *smtpmock.Message, user *models.User) error { for _, responses := range message.RcpttoRequestResponse() { if !strings.Contains(responses[0], "RCPT TO:<"+user.Email) { return fmt.Errorf("Registration Information didn't reach client! Recipient was: %v instead of %v", responses[0], user.Email) } } // the email is encoded with a lowercase %3d while the url.encodeQuery returns an uppercase %3D. Therefore we remove the last char(padded base64 '=' if !strings.Contains(message.MsgRequest(), user.Verification.VerificationToken[:len(user.Verification.VerificationToken)-1]) { return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", user.Verification.VerificationToken, message.MsgRequest()) } if !strings.Contains(message.MsgRequest(), config.BaseURL) { return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.BaseURL) } return nil } func initSubscriptionPlans() error { subscription := models.SubscriptionModel{ Name: "Basic", Details: "Test Plan", MonthlyFee: 2, HourlyRate: 3, } result := database.DB.Create(&subscription) if result.Error != nil { return result.Error } return nil } func getMockedContext(jsonStr []byte) (*gin.Context, *httptest.ResponseRecorder) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) var err error c.Request, err = http.NewRequest("POST", "/register", bytes.NewBuffer(jsonStr)) if err != nil { logger.Error.Fatalf("Failed to create new Request: %#v", err) } c.Request.Header.Set("Content-Type", "application/json") return c, w } func deleteTestDB(dbPath string) error { err := os.Remove(dbPath) if err != nil { return err } return 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 generateInputJSON(customize func(models.User) models.User) string { user := getBaseUser() user = customize(user) // Apply the customization regData := RegistrationData{User: user} jsonBytes, err := json.Marshal(regData) if err != nil { logger.Error.Printf("couldn't generate Json from User: %#v\nERROR: %#v", regData, err) return "" } return string(jsonBytes) } func getTestUsers() []test { return []test{ { name: "birthday < 18 should fail", wantResponse: http.StatusNotAcceptable, wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, assert: false, input: generateInputJSON(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(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(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(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(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(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(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(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(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(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(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(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(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(func(user models.User) models.User { user.Email = "john.doe2@example.com" user.Company = "ACME" return user }), }, } }