From ae0ec8bf8885f1589c2dcc2bcf330e391fbb7137 Mon Sep 17 00:00:00 2001 From: "$(pass /github/name)" <$(pass /github/email)> Date: Sun, 18 Aug 2024 14:17:17 +0200 Subject: [PATCH] add: contactController,tests & refactored tests --- configs/config.template.json | 6 +- internal/config/config.go | 28 +- internal/controllers/contactController.go | 21 +- .../controllers/contactController_test.go | 157 ++++++ internal/controllers/controllers_test.go | 178 ++++++ internal/controllers/membershipController.go | 10 +- .../controllers/membershipController_test.go | 119 ++++ internal/controllers/user_controller.go | 3 +- internal/controllers/user_controller_test.go | 525 +++++++++--------- .../subscription_model_repository.go | 15 + internal/routes/routes.go | 2 +- internal/server/server.go | 5 +- internal/services/email_service.go | 25 +- internal/services/membership_service.go | 24 +- internal/services/user_service.go | 3 +- internal/utils/crypto.go | 83 +++ templates/email/mail_welcome.html | 2 +- 17 files changed, 886 insertions(+), 320 deletions(-) create mode 100644 internal/controllers/contactController_test.go create mode 100644 internal/controllers/controllers_test.go create mode 100644 internal/controllers/membershipController_test.go diff --git a/configs/config.template.json b/configs/config.template.json index 076ebe1..b2a5c80 100644 --- a/configs/config.template.json +++ b/configs/config.template.json @@ -1,5 +1,5 @@ { - "base_url": "https://domain.de", + "BaseURL": "https://domain.de", "db": { "Path": "data/db.sqlite3" }, @@ -18,5 +18,9 @@ }, "auth": { "APIKey": "" + }, + "recipients": { + "ContactForm": "contacts@server.com", + "UserRegistration": "registration@server.com" } } diff --git a/internal/config/config.go b/internal/config/config.go index ca46401..f28ffaf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -43,23 +43,30 @@ type TemplateConfig struct { StaticPath string `json:"StaticPath" default:"templates/css" envconfig:"TEMPLATE_STATIC_PATH"` } +type RecipientsConfig struct { + ContactForm string `json:"ContactForm" envconfig:"RECIPIENT_CONTACT_FORM"` + UserRegistration string `json:"UserRegistration" envconfig:"RECIPIENT_USER_REGISTRATION"` +} + 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"` + Recipients RecipientsConfig `json:"recipients"` + ConfigFilePath string `json:"config_file_path" envconfig:"CONFIG_FILE_PATH"` + BaseURL string `json:"BaseUrl" envconfig:"BASE_URL"` + DB DatabaseConfig `json:"db"` SMTP SMTPConfig `json:"smtp"` } var ( - BaseURL string - CFGPath string - CFG Config - Auth AuthenticationConfig - DB DatabaseConfig - Templates TemplateConfig - SMTP SMTPConfig + BaseURL string + CFGPath string + CFG Config + Auth AuthenticationConfig + DB DatabaseConfig + Templates TemplateConfig + SMTP SMTPConfig + Recipients RecipientsConfig ) // LoadConfig initializes the configuration by reading from a file and environment variables. @@ -86,6 +93,7 @@ func LoadConfig() { Templates = CFG.Templates SMTP = CFG.SMTP BaseURL = CFG.BaseURL + Recipients = CFG.Recipients } // readFile reads the configuration from the specified file path into the provided Config struct. diff --git a/internal/controllers/contactController.go b/internal/controllers/contactController.go index 128fa79..28ca706 100644 --- a/internal/controllers/contactController.go +++ b/internal/controllers/contactController.go @@ -15,30 +15,33 @@ type ContactController struct { EmailService *services.EmailService } type contactData struct { - email string `validate:"required,email"` - name string - message string `validate:"required"` + Email string `form:"email" validate:"required,email"` + Name string `form:"name"` + Message string `form:"message" validate:"required"` + Honeypot string `form:"username" validate:"eq="` } func (cc *ContactController) RelayContactRequest(c *gin.Context) { var msgData contactData - if c.Query("username") != "" { + + if err := c.ShouldBind(&msgData); err != nil { // A bot is talking to us + c.JSON(http.StatusNotAcceptable, gin.H{"error": "Not Acceptable"}) return } - msgData.name = c.Query("name") - msgData.email = c.Query("email") - msgData.message = c.Query("message") validate := validator.New() if err := validate.Struct(msgData); err != nil { logger.Error.Printf("Couldn't validate contact form data: %v", err) c.JSON(http.StatusNotAcceptable, gin.H{"error": "Couldn't validate contact form data"}) + return } - if err := cc.EmailService.RelayContactFormMessage(&msgData.email, &msgData.name, &msgData.message); err != nil { + if err := cc.EmailService.RelayContactFormMessage(msgData.Email, msgData.Name, msgData.Message); err != nil { logger.Error.Printf("Couldn't send contact message mail: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't send mail"}) + return } - c.JSON(http.StatusOK, "Your message has been sent") + + c.JSON(http.StatusAccepted, "Your message has been sent") } diff --git a/internal/controllers/contactController_test.go b/internal/controllers/contactController_test.go new file mode 100644 index 0000000..5beb71f --- /dev/null +++ b/internal/controllers/contactController_test.go @@ -0,0 +1,157 @@ +package controllers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "GoMembership/internal/config" + "GoMembership/internal/constants" + "GoMembership/internal/utils" + + "github.com/gin-gonic/gin" +) + +type RelayContactRequestTest struct { + Input url.Values + Name string + WantResponse int + Assert bool +} + +func TestContactController(t *testing.T) { + + tests := getContactData() + 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 (rt *RelayContactRequestTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder) { + return GetMockedFormContext(rt.Input, "/contact") +} + +func (rt *RelayContactRequestTest) RunHandler(c *gin.Context) { + Cc.RelayContactRequest(c) +} + +func (rt *RelayContactRequestTest) 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 *RelayContactRequestTest) ValidateResult() error { + + messages := utils.SMTPGetMessages() + + for _, message := range messages { + + mail, err := utils.DecodeMail(message.MsgRequest()) + if err != nil { + return err + } + if strings.Contains(mail.Subject, constants.MailContactSubject) { + + if err := checkContactRequestMail(mail, rt); err != nil { + return err + } + } else { + return fmt.Errorf("Subject not expected: %v", mail.Subject) + } + } + return nil +} + +func checkContactRequestMail(mail *utils.Email, rt *RelayContactRequestTest) error { + + if !strings.Contains(mail.To, config.Recipients.ContactForm) { + return fmt.Errorf("Contact Information didn't reach the admin! Recipient was: %v instead of %v", mail.To, config.Recipients.ContactForm) + } + if !strings.Contains(mail.From, config.SMTP.User) { + return fmt.Errorf("Contact Information was sent from unexpected address! Sender was: %v instead of %v", mail.From, config.SMTP.User) + } + + //Check if all the relevant data has been passed to the mail. + if !strings.Contains(mail.Body, rt.Input.Get("name")) { + return fmt.Errorf("User name(%v) has not been rendered in contact mail.", rt.Input.Get("name")) + } + + if !strings.Contains(mail.Body, rt.Input.Get("message")) { + return fmt.Errorf("User message(%v) has not been rendered in contact mail.", rt.Input.Get("message")) + } + return nil +} + +func getBaseRequest() *url.Values { + return &url.Values{ + "username": {""}, + "name": {"My-First and-Last-Name"}, + "email": {"name@domain.de"}, + "message": {"My message to the world"}, + } +} + +func customizeRequest(updates map[string]string) *url.Values { + form := getBaseRequest() + for key, value := range updates { + form.Set(key, value) + } + return form +} + +func getContactData() []RelayContactRequestTest { + return []RelayContactRequestTest{ + { + Name: "mail empty, should fail", + WantResponse: http.StatusNotAcceptable, + Assert: false, + Input: *customizeRequest( + map[string]string{ + "email": "", + }), + }, + { + Name: "mail invalid, should fail", + WantResponse: http.StatusNotAcceptable, + Assert: false, + Input: *customizeRequest( + map[string]string{ + "email": "novalid#email.de", + }), + }, + { + Name: "No message should fail", + WantResponse: http.StatusNotAcceptable, + Assert: true, + Input: *customizeRequest( + map[string]string{ + "message": "", + }), + }, + { + Name: "Honeypot set, should fail", + WantResponse: http.StatusNotAcceptable, + Assert: true, + Input: *customizeRequest( + map[string]string{ + "username": "I'm a bot", + }), + }, + { + Name: "Correct message, should pass", + WantResponse: http.StatusAccepted, + Assert: true, + Input: *customizeRequest( + map[string]string{}), + }, + } +} diff --git a/internal/controllers/controllers_test.go b/internal/controllers/controllers_test.go new file mode 100644 index 0000000..f9280f4 --- /dev/null +++ b/internal/controllers/controllers_test.go @@ -0,0 +1,178 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strconv" + "testing" + + "log" + + "github.com/gin-gonic/gin" + + "GoMembership/internal/config" + "GoMembership/internal/database" + "GoMembership/internal/models" + "GoMembership/internal/repositories" + "GoMembership/internal/services" + "GoMembership/internal/utils" + "GoMembership/pkg/logger" +) + +type TestCase interface { + SetupContext() (*gin.Context, *httptest.ResponseRecorder) + RunHandler(*gin.Context) + ValidateResponse(*httptest.ResponseRecorder) error + ValidateResult() error +} + +const ( + Host = "127.0.0.1" + Port int = 2525 +) + +var ( + Uc *UserController + Mc *MembershipController + Cc *ContactController +) + +func TestSuite(t *testing.T) { + _ = deleteTestDB("test.db") + if err := database.InitDB("test.db"); err != nil { + log.Fatalf("Failed to create DB: %#v", err) + } + + if err := os.Setenv("SMTP_HOST", Host); err != nil { + log.Fatalf("Error setting environment variable: %v", err) + } + if err := os.Setenv("SMTP_PORT", strconv.Itoa(Port)); err != nil { + log.Fatalf("Error setting environment variable: %v", err) + } + if err := os.Setenv("BASE_URL", "http://"+Host+":2525"); err != nil { + log.Fatalf("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) + 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} + Mc = &MembershipController{Service: *membershipService} + Cc = &ContactController{EmailService: emailService} + + if err := initSubscriptionPlans(); err != nil { + log.Fatalf("Failed to init Subscription plans: %#v", err) + } + + // Run all tests + // code := m.Run() + + t.Run("userController", func(t *testing.T) { + TestUserController(t) + }) + + t.Run("contactController", func(t *testing.T) { + TestContactController(t) + }) + + t.Run("membershipController", func(t *testing.T) { + TestMembershipController(t) + }) + + if err := utils.SMTPStop(); err != nil { + log.Fatalf("Failed to stop SMTP Mockup Server: %#v", err) + } + + if err := deleteTestDB("test.db"); err != nil { + log.Fatalf("Failed to tear down DB: %#v", err) + } +} + +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 GetMockedJSONContext(jsonStr []byte, url string) (*gin.Context, *httptest.ResponseRecorder) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + var err error + c.Request, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) + if err != nil { + log.Fatalf("Failed to create new Request: %#v", err) + } + c.Request.Header.Set("Content-Type", "application/json") + + return c, w +} + +func GetMockedFormContext(formData url.Values, url string) (*gin.Context, *httptest.ResponseRecorder) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + req, err := http.NewRequest("POST", url, bytes.NewBufferString(formData.Encode())) + if err != nil { + log.Fatalf("Failed to create new Request: %#v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + c.Request = req + + return c, w +} + +func deleteTestDB(dbPath string) error { + err := os.Remove(dbPath) + if err != nil { + return err + } + return nil +} + +func runSingleTest(tc TestCase) error { + c, w := tc.SetupContext() + tc.RunHandler(c) + + if err := tc.ValidateResponse(w); err != nil { + return err + } + + return tc.ValidateResult() +} + +func GenerateInputJSON(aStruct interface{}) string { + + // Marshal the object into JSON + jsonBytes, err := json.Marshal(aStruct) + if err != nil { + logger.Error.Fatalf("Couldn't generate JSON: %#v\nERROR: %#v", aStruct, err) + return "" + } + return string(jsonBytes) +} diff --git a/internal/controllers/membershipController.go b/internal/controllers/membershipController.go index 30c461d..f944d30 100644 --- a/internal/controllers/membershipController.go +++ b/internal/controllers/membershipController.go @@ -40,15 +40,17 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) { c.JSON(http.StatusExpectationFailed, "API Key is missing") return } - logger.Info.Printf("registering subscription: %+v", regData) // Register Subscription id, err := mc.Service.RegisterSubscription(®Data.Model) if err != nil { logger.Error.Printf("Couldn't register Membershipmodel: %v", err) - c.JSON(http.StatusInternalServerError, "Couldn't register Membershipmodel") + c.JSON(int(id), "Couldn't register Membershipmodel") return } - regData.Model.ID = id - + logger.Info.Printf("registering subscription: %+v", regData) + c.JSON(http.StatusCreated, gin.H{ + "status": "success", + "id": id, + }) } diff --git a/internal/controllers/membershipController_test.go b/internal/controllers/membershipController_test.go new file mode 100644 index 0000000..c234f59 --- /dev/null +++ b/internal/controllers/membershipController_test.go @@ -0,0 +1,119 @@ +package controllers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "GoMembership/internal/config" + "GoMembership/internal/models" + + "github.com/gin-gonic/gin" +) + +type RegisterSubscriptionTest struct { + WantDBData map[string]interface{} + Input string + Name string + WantResponse int + Assert bool +} + +func TestMembershipController(t *testing.T) { + + tests := getSubscriptionData() + 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 (rt *RegisterSubscriptionTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder) { + return GetMockedJSONContext([]byte(rt.Input), "register/subscription") +} + +func (rt *RegisterSubscriptionTest) RunHandler(c *gin.Context) { + Mc.RegisterSubscription(c) +} + +func (rt *RegisterSubscriptionTest) 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 *RegisterSubscriptionTest) ValidateResult() error { + return validateSubscription(rt.Assert, rt.WantDBData) +} + +func validateSubscription(assert bool, wantDBData map[string]interface{}) error { + subscriptions, err := Mc.Service.GetSubscriptions(wantDBData) + if err != nil { + return fmt.Errorf("Error in database ops: %#v", err) + } + if assert != (len(*subscriptions) != 0) { + return fmt.Errorf("Subscription entry query didn't met expectation: %v != %#v", assert, *subscriptions) + } + return nil +} + +func getBaseSubscription() MembershipData { + return MembershipData{ + APIKey: config.Auth.APIKEY, + Model: models.SubscriptionModel{ + Name: "Just a Subscription", + Details: "A subscription detail", + MonthlyFee: 12.0, + HourlyRate: 14.0, + }, + } +} +func customizeSubscription(customize func(MembershipData) MembershipData) MembershipData { + subscription := getBaseSubscription() + return customize(subscription) +} + +func getSubscriptionData() []RegisterSubscriptionTest { + return []RegisterSubscriptionTest{ + { + Name: "No Details should fail", + WantResponse: http.StatusNotAcceptable, + WantDBData: map[string]interface{}{"name": "Just a Subscription"}, + Assert: false, + Input: GenerateInputJSON( + customizeSubscription(func(subscription MembershipData) MembershipData { + subscription.Model.Details = "" + return subscription + })), + }, + { + Name: "No Model Name should fail", + WantResponse: http.StatusNotAcceptable, + WantDBData: map[string]interface{}{"name": ""}, + Assert: false, + Input: GenerateInputJSON( + customizeSubscription(func(subscription MembershipData) MembershipData { + subscription.Model.Name = "" + return subscription + })), + }, + { + Name: "correct entry should pass", + WantResponse: http.StatusCreated, + WantDBData: map[string]interface{}{"name": "Just a Subscription"}, + Assert: true, + Input: GenerateInputJSON( + customizeSubscription(func(subscription MembershipData) MembershipData { + subscription.Model.Conditions = "Some Condition" + subscription.Model.IncludedPerYear = 0 + subscription.Model.IncludedPerMonth = 1 + return subscription + })), + }, + } +} diff --git a/internal/controllers/user_controller.go b/internal/controllers/user_controller.go index 354637e..5c6e816 100644 --- a/internal/controllers/user_controller.go +++ b/internal/controllers/user_controller.go @@ -88,7 +88,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) { } // Notify admin of new user registration - if err := uc.EmailService.NotifyAdminOfNewUser(®Data.User); err != nil { + if err := uc.EmailService.SendRegistrationNotification(®Data.User); err != nil { logger.Error.Printf("Failed to notify admin of new user registration: %v", err) // Proceed without returning error since user registration is successful } @@ -112,6 +112,7 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) { c.HTML(http.StatusUnauthorized, "verification_error.html", gin.H{"ErrorMessage": "Emailadresse wurde schon bestätigt. Sollte dies nicht der Fall sein, wende Dich bitte an info@carsharing-hasloh.de."}) return } + logger.Info.Printf("User: %#v", user) uc.EmailService.SendWelcomeEmail(user) c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName}) diff --git a/internal/controllers/user_controller_test.go b/internal/controllers/user_controller_test.go index 06eb4e7..8d7d465 100644 --- a/internal/controllers/user_controller_test.go +++ b/internal/controllers/user_controller_test.go @@ -1,130 +1,66 @@ package controllers import ( - "bytes" - "encoding/json" "fmt" - "strconv" - "strings" - "net/http" "net/http/httptest" - "os" + "net/url" + "path/filepath" + "regexp" + "strings" "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 RegisterUserTest struct { + WantDBData map[string]interface{} + Name string + Input string + WantResponse int + Assert bool } -// type RegistrationData struct { -// User models.User `json:"user"` -// } +func (rt *RegisterUserTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder) { + return GetMockedJSONContext([]byte(rt.Input), "register") +} -const ( - Host = "127.0.0.1" - Port int = 2525 - User = "alex@mail.de" - Pass = "secret" - AdminMail = "admin@mail.de" -) +func (rt *RegisterUserTest) RunHandler(c *gin.Context) { + Uc.RegisterUser(c) +} -var ( - uc UserController -) +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) { - _ = 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) { + 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) + users, err := Uc.Service.GetUsers(wantDBData) if err != nil { return fmt.Errorf("Error in database ops: %#v", err) } @@ -137,131 +73,179 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error { //check for email delivery messages := utils.SMTPGetMessages() for _, message := range messages { - - if strings.Contains(message.MsgRequest(), constants.MailWelcomeSubject) { - if err := checkWelcomeMail(&message); err != nil { + 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(message.MsgRequest(), constants.MailRegistrationSubject) { - if err := checkRegistrationMail(&message, &(*users)[0]); err != nil { + } else if strings.Contains(mail.Subject, constants.MailVerificationSubject) { + if err := checkVerificationMail(mail, &(*users)[0]); err != nil { return err } - } else if strings.Contains(message.MsgRequest(), constants.MailVerificationSubject) { - if err := checkVerificationMail(&message, &(*users)[0]); err != nil { + 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", message.MsgRequest()) + return fmt.Errorf("Subject not expected: %v", mail.Subject) } } } return nil } -func checkWelcomeMail(message *smtpmock.Message) error { - return nil -} +func checkWelcomeMail(message *utils.Email, user *models.User) error { -func checkRegistrationMail(message *smtpmock.Message, 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) + } - 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.Body, user.FirstName) { + return fmt.Errorf("User first name(%v) has not been rendered in registration mail.", user.FirstName) } - if !strings.Contains(message.MsgRequest(), fmt.Sprintf("Preis/Monat: %v", user.Membership.SubscriptionModel.MonthlyFee)) { + 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.MsgRequest(), fmt.Sprintf("Preis/h: %v", user.Membership.SubscriptionModel.HourlyRate)) { + 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.MsgRequest(), user.Company) { + 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.MsgRequest(), fmt.Sprintf("Mitgliedsnr: %v", user.Membership.ID)) { + 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.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) { + 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 *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) - } +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) } - // 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.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) } - if !strings.Contains(message.MsgRequest(), config.BaseURL) { + //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) } - 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 { + // 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, "*")) // Adjust the path to your HTML templates + + 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: @@ -283,158 +267,151 @@ func getBaseUser() models.User { } } -func generateInputJSON(customize func(models.User) models.User) string { +func customizeInput(customize func(models.User) models.User) *RegistrationData { 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) + return &RegistrationData{User: user} } -func getTestUsers() []test { - return []test{ +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(func(user models.User) models.User { + 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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.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(func(user models.User) models.User { 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(func(user models.User) models.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(func(user models.User) models.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 - }), + })), }, } } diff --git a/internal/repositories/subscription_model_repository.go b/internal/repositories/subscription_model_repository.go index 3a75492..4edb11d 100644 --- a/internal/repositories/subscription_model_repository.go +++ b/internal/repositories/subscription_model_repository.go @@ -3,6 +3,8 @@ package repositories import ( "GoMembership/internal/database" + "gorm.io/gorm" + "GoMembership/internal/models" ) @@ -10,6 +12,7 @@ type SubscriptionModelsRepositoryInterface interface { CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (int64, error) GetMembershipModelNames() ([]string, error) GetModelByName(modelname *string) (*models.SubscriptionModel, error) + GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) } type SubscriptionModelsRepository struct{} @@ -38,3 +41,15 @@ func (sr *SubscriptionModelsRepository) GetMembershipModelNames() ([]string, err } return names, nil } + +func (sr *SubscriptionModelsRepository) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) { + var subscriptions []models.SubscriptionModel + result := database.DB.Where(where).Find(&subscriptions) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil, gorm.ErrRecordNotFound + } + return nil, result.Error + } + return &subscriptions, nil +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index a30c73d..7a99eaa 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" ) -func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController) { +func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController) { router.GET("/backend/verify", userController.VerifyMailHandler) router.POST("/backend/api/register", userController.RegisterUser) router.POST("/backend/api/register/subscription", membershipcontroller.RegisterSubscription) diff --git a/internal/server/server.go b/internal/server/server.go index 88958fa..8573d50 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -30,7 +30,7 @@ func Run() { logger.Error.Fatalf("Couldn't init database: %v", err) } - emailService := services.NewEmailService(config.SMTP.Host, config.SMTP.Port, config.SMTP.User, config.SMTP.Password, config.SMTP.AdminEmail) + emailService := services.NewEmailService(config.SMTP.Host, config.SMTP.Port, config.SMTP.User, config.SMTP.Password) var consentRepo repositories.ConsentRepositoryInterface = &repositories.ConsentRepository{} consentService := &services.ConsentService{Repo: consentRepo} @@ -47,6 +47,7 @@ func Run() { userController := &controllers.UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService} membershipController := &controllers.MembershipController{Service: *membershipService} + contactController := &controllers.ContactController{EmailService: emailService} router := gin.Default() // gin.SetMode(gin.ReleaseMode) router.Static(config.Templates.StaticPath, "./style") @@ -55,7 +56,7 @@ func Run() { router.Use(gin.Logger()) // router.Use(middlewares.LoggerMiddleware()) - routes.RegisterRoutes(router, userController, membershipController) + routes.RegisterRoutes(router, userController, membershipController, contactController) // create subrouter for teh authenticated area /account // also pthprefix matches everything below /account // accountRouter := router.PathPrefix("/account").Subrouter() diff --git a/internal/services/email_service.go b/internal/services/email_service.go index 18c6ea0..7334b5e 100644 --- a/internal/services/email_service.go +++ b/internal/services/email_service.go @@ -13,13 +13,12 @@ import ( ) type EmailService struct { - dialer *gomail.Dialer - adminEmail string + dialer *gomail.Dialer } -func NewEmailService(host string, port int, username, password, adminEmail string) *EmailService { +func NewEmailService(host string, port int, username string, password string) *EmailService { dialer := gomail.NewDialer(host, port, username, password) - return &EmailService{dialer: dialer, adminEmail: adminEmail} + return &EmailService{dialer: dialer} } func (s *EmailService) SendEmail(to string, subject string, body string) error { @@ -87,10 +86,10 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error { Company string FirstName string MembershipModel string + BASEURL string MembershipID int64 MembershipFee float32 RentalFee float32 - BASEURL string }{ Company: user.Company, FirstName: user.FirstName, @@ -110,24 +109,24 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error { return s.SendEmail(user.Email, subject, body) } -func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error { +func (s *EmailService) SendRegistrationNotification(user *models.User) error { // Prepare data to be injected into the template data := struct { - City string - Email string + FirstName string + DateOfBirth string LastName string MembershipModel string Address string IBAN string - FirstName string + Email string Phone string - DateOfBirth string + City string Company string ZipCode string + BASEURL string MembershipID int64 RentalFee float32 MembershipFee float32 - BASEURL string }{ Company: user.Company, FirstName: user.FirstName, @@ -152,7 +151,7 @@ func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error { logger.Error.Print("Couldn't send admin notification mail") return err } - return s.SendEmail(config.SMTP.AdminEmail, subject, body) + return s.SendEmail(config.Recipients.UserRegistration, subject, body) } func (s *EmailService) RelayContactFormMessage(sender string, name string, message string) error { @@ -171,5 +170,5 @@ func (s *EmailService) RelayContactFormMessage(sender string, name string, messa logger.Error.Print("Couldn't send contact form message mail") return err } - return s.SendEmail(config.SMTP.AdminEmail, subject, body) + return s.SendEmail(config.Recipients.ContactForm, subject, body) } diff --git a/internal/services/membership_service.go b/internal/services/membership_service.go index a909724..8124846 100644 --- a/internal/services/membership_service.go +++ b/internal/services/membership_service.go @@ -1,11 +1,15 @@ package services import ( + "net/http" + "slices" + "time" + + "github.com/go-playground/validator/v10" + "GoMembership/internal/models" "GoMembership/internal/repositories" "GoMembership/pkg/errors" - "slices" - "time" ) type MembershipServiceInterface interface { @@ -14,6 +18,7 @@ type MembershipServiceInterface interface { RegisterSubscription(subscription *models.SubscriptionModel) (int64, error) GetMembershipModelNames() ([]string, error) GetModelByName(modelname *string) (*models.SubscriptionModel, error) + GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) } type MembershipService struct { @@ -32,6 +37,9 @@ func (service *MembershipService) FindMembershipByUserID(userID int64) (*models. // Membership_Subscriptions func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (int64, error) { + if err := validateSubscriptionData(subscription); err != nil { + return http.StatusNotAcceptable, err + } return service.SubscriptionRepo.CreateSubscriptionModel(subscription) } @@ -50,3 +58,15 @@ func (service *MembershipService) GetModelByName(modelname *string) (*models.Sub } return service.SubscriptionRepo.GetModelByName(modelname) } + +func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) { + return service.SubscriptionRepo.GetSubscriptions(where) +} + +func validateSubscriptionData(subscription *models.SubscriptionModel) error { + validate := validator.New() + + validate.RegisterValidation("subscriptionModel", func(fl validator.FieldLevel) bool { return true }) + validate.RegisterValidation("membershipField", func(fl validator.FieldLevel) bool { return true }) + return validate.Struct(subscription) +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go index f70878a..9a534b3 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -34,8 +34,7 @@ func (service *UserService) RegisterUser(user *models.User) (int64, string, erro } user.Salt = base64.StdEncoding.EncodeToString(salt) */ - err := validateRegistrationData(user) - if err != nil { + if err := validateRegistrationData(user); err != nil { return http.StatusNotAcceptable, "", err } diff --git a/internal/utils/crypto.go b/internal/utils/crypto.go index bf21050..d6aa2f2 100644 --- a/internal/utils/crypto.go +++ b/internal/utils/crypto.go @@ -1,10 +1,26 @@ package utils import ( + "bytes" "crypto/rand" "encoding/base64" + "io" + "mime" + "mime/quotedprintable" + "net/mail" + "strings" ) +type Email struct { + MimeVersion string + Date string + From string + To string + Subject string + ContentType string + Body string +} + func GenerateRandomString(length int) (string, error) { bytes := make([]byte, length) _, err := rand.Read(bytes) @@ -17,3 +33,70 @@ func GenerateRandomString(length int) (string, error) { func GenerateVerificationToken() (string, error) { return GenerateRandomString(32) } + +func DecodeMail(message string) (*Email, error) { + msg, err := mail.ReadMessage(strings.NewReader(message)) + if err != nil { + return nil, err + } + decodedBody, err := io.ReadAll(msg.Body) + if err != nil { + return nil, err + } + decodedBodyString, err := DecodeQuotedPrintable(string(decodedBody)) + if err != nil { + return nil, err + } + decodedSubject, err := DecodeRFC2047(msg.Header.Get("Subject")) + if err != nil { + return nil, err + } + + email := &Email{} + + // Populate the headers + email.MimeVersion = msg.Header.Get("Mime-Version") + email.Date = msg.Header.Get("Date") + email.From = msg.Header.Get("From") + email.To = msg.Header.Get("To") + email.Subject = decodedSubject + email.Body = decodedBodyString + email.ContentType = msg.Header.Get("Content-Type") + + return email, nil +} + +func DecodeRFC2047(encoded string) (string, error) { + decoder := new(mime.WordDecoder) + decoded, err := decoder.DecodeHeader(encoded) + if err != nil { + return "", err + } + return decoded, nil +} + +func DecodeQuotedPrintable(encodedString string) (string, error) { + // Decode quoted-printable encoding + reader := quotedprintable.NewReader(strings.NewReader(encodedString)) + decodedBytes := new(bytes.Buffer) + _, err := decodedBytes.ReadFrom(reader) + if err != nil { + return "", err + } + return decodedBytes.String(), nil +} + +func EncodeQuotedPrintable(s string) string { + var buf bytes.Buffer + + // Use Quoted-Printable encoder + qp := quotedprintable.NewWriter(&buf) + + // Write the UTF-8 encoded string to the Quoted-Printable encoder + qp.Write([]byte(s)) + qp.Close() + + // Encode the result into a MIME header + return mime.QEncoding.Encode("UTF-8", buf.String()) + +} diff --git a/templates/email/mail_welcome.html b/templates/email/mail_welcome.html index f617cef..c63ead6 100644 --- a/templates/email/mail_welcome.html +++ b/templates/email/mail_welcome.html @@ -55,7 +55,7 @@ Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur erfolgreichen E-Mail-Verifikation und willkommen in unserem Verein! - +