diff --git a/configs/config.template.json b/configs/config.template.json index 95f15ac..076ebe1 100644 --- a/configs/config.template.json +++ b/configs/config.template.json @@ -1,4 +1,5 @@ { + "base_url": "https://domain.de", "db": { "Path": "data/db.sqlite3" }, diff --git a/internal/config/config.go b/internal/config/config.go index 29b7ec3..ca46401 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,6 +45,7 @@ type TemplateConfig struct { type Config struct { ConfigFilePath string `json:"config_file_path" envconfig:"CONFIG_FILE_PATH"` + BaseURL string `json:"base_url" envconfig:"BASE_URL"` Auth AuthenticationConfig `json:"auth"` DB DatabaseConfig `json:"db"` Templates TemplateConfig `json:"templates"` @@ -52,6 +53,7 @@ type Config struct { } var ( + BaseURL string CFGPath string CFG Config Auth AuthenticationConfig @@ -83,6 +85,7 @@ func LoadConfig() { DB = CFG.DB Templates = CFG.Templates SMTP = CFG.SMTP + BaseURL = CFG.BaseURL } // readFile reads the configuration from the specified file path into the provided Config struct. diff --git a/internal/constants/constants.go b/internal/constants/constants.go index a26fd69..2e98def 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -9,4 +9,7 @@ const ( DelayedPaymentStatus SettledPaymentStatus AwaitingPaymentStatus + MailVerificationSubject = "Nur noch ein kleiner Schritt!" + MailRegistrationSubject = "Neues Mitglied hat sich registriert" + MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V." ) diff --git a/internal/controllers/user_controller_test.go b/internal/controllers/user_controller_test.go index 74f0eb5..06eb4e7 100644 --- a/internal/controllers/user_controller_test.go +++ b/internal/controllers/user_controller_test.go @@ -3,9 +3,10 @@ package controllers import ( "bytes" "encoding/json" + "fmt" "strconv" + "strings" - // "io/ioutil" "net/http" "net/http/httptest" "os" @@ -13,9 +14,10 @@ import ( "time" "github.com/gin-gonic/gin" - "gorm.io/gorm" + smtpmock "github.com/mocktools/go-smtp-mock/v2" "GoMembership/internal/config" + "GoMembership/internal/constants" "GoMembership/internal/database" "GoMembership/internal/models" "GoMembership/internal/repositories" @@ -95,45 +97,137 @@ func TestUserController(t *testing.T) { tests := getTestUsers() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - runSingleTest(t, &tt) + 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(t *testing.T, tt *test) { +func runSingleTest(tt *test) error { c, w := getMockedContext([]byte(tt.input)) uc.RegisterUser(c) if w.Code != int(tt.wantResponse) { - t.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, tt.wantResponse) + return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, tt.wantResponse) } - validateUser(t, tt.assert, tt.wantDBData) + return validateUser(tt.assert, tt.wantDBData) } -func handleError(t *testing.T, err error, assert bool) { - if err == gorm.ErrRecordNotFound && !assert { - return // Expected case: user not found and assertion is false - } - - if err != nil { - t.Errorf("Error during testing: %#v", err) - } -} - -func validateUser(t *testing.T, assert bool, wantDBData map[string]interface{}) { +func validateUser(assert bool, wantDBData map[string]interface{}) error { users, err := uc.Service.GetUsers(wantDBData) if err != nil { - t.Errorf("Error in database ops: %#v", err) + return fmt.Errorf("Error in database ops: %#v", err) } + if assert != (len(*users) != 0) { - t.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users) + 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", @@ -331,5 +425,16 @@ func getTestUsers() []test { 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 + }), + }, } } diff --git a/internal/services/email_service.go b/internal/services/email_service.go index b032550..244c3a2 100644 --- a/internal/services/email_service.go +++ b/internal/services/email_service.go @@ -2,6 +2,7 @@ package services import ( "GoMembership/internal/config" + "GoMembership/internal/constants" "GoMembership/internal/models" "GoMembership/pkg/logger" "bytes" @@ -60,13 +61,15 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e FirstName string LastName string Token string + BASEURL string }{ FirstName: user.FirstName, LastName: user.LastName, Token: *token, + BASEURL: config.BaseURL, } - subject := "Nur noch ein kleiner Schritt!" + subject := constants.MailVerificationSubject body, err := ParseTemplate("mail_verification.html", data) if err != nil { logger.Error.Print("Couldn't send verification mail") @@ -85,6 +88,7 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error { MembershipID int64 MembershipFee float32 RentalFee float32 + BASEURL string }{ Company: user.Company, FirstName: user.FirstName, @@ -92,9 +96,10 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error { MembershipID: user.Membership.ID, MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee), RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate), + BASEURL: config.BaseURL, } - subject := "Willkommen beim Dörpsmobil Hasloh e.V." + subject := constants.MailWelcomeSubject body, err := ParseTemplate("mail_welcome.html", data) if err != nil { logger.Error.Print("Couldn't send welcome mail") @@ -120,6 +125,7 @@ func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error { MembershipID int64 RentalFee float32 MembershipFee float32 + BASEURL string }{ Company: user.Company, FirstName: user.FirstName, @@ -135,9 +141,10 @@ func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error { Email: user.Email, Phone: user.Phone, IBAN: user.BankAccount.IBAN, + BASEURL: config.BaseURL, } - subject := "Neues Mitglied hat sich registriert" + subject := constants.MailRegistrationSubject body, err := ParseTemplate("mail_registration.html", data) if err != nil { logger.Error.Print("Couldn't send admin notification mail") diff --git a/templates/email/mail_registration.html b/templates/email/mail_registration.html index 179b12d..f5fab32 100644 --- a/templates/email/mail_registration.html +++ b/templates/email/mail_registration.html @@ -31,12 +31,12 @@