add: contactController,tests & refactored tests

This commit is contained in:
$(pass /github/name)
2024-08-18 14:17:17 +02:00
parent 6441ad3ef9
commit ae0ec8bf88
17 changed files with 886 additions and 320 deletions

View File

@@ -1,5 +1,5 @@
{ {
"base_url": "https://domain.de", "BaseURL": "https://domain.de",
"db": { "db": {
"Path": "data/db.sqlite3" "Path": "data/db.sqlite3"
}, },
@@ -18,5 +18,9 @@
}, },
"auth": { "auth": {
"APIKey": "" "APIKey": ""
},
"recipients": {
"ContactForm": "contacts@server.com",
"UserRegistration": "registration@server.com"
} }
} }

View File

@@ -43,23 +43,30 @@ type TemplateConfig struct {
StaticPath string `json:"StaticPath" default:"templates/css" envconfig:"TEMPLATE_STATIC_PATH"` 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 { 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"` Auth AuthenticationConfig `json:"auth"`
DB DatabaseConfig `json:"db"`
Templates TemplateConfig `json:"templates"` 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"` SMTP SMTPConfig `json:"smtp"`
} }
var ( var (
BaseURL string BaseURL string
CFGPath string CFGPath string
CFG Config CFG Config
Auth AuthenticationConfig Auth AuthenticationConfig
DB DatabaseConfig DB DatabaseConfig
Templates TemplateConfig Templates TemplateConfig
SMTP SMTPConfig SMTP SMTPConfig
Recipients RecipientsConfig
) )
// LoadConfig initializes the configuration by reading from a file and environment variables. // LoadConfig initializes the configuration by reading from a file and environment variables.
@@ -86,6 +93,7 @@ func LoadConfig() {
Templates = CFG.Templates Templates = CFG.Templates
SMTP = CFG.SMTP SMTP = CFG.SMTP
BaseURL = CFG.BaseURL BaseURL = CFG.BaseURL
Recipients = CFG.Recipients
} }
// readFile reads the configuration from the specified file path into the provided Config struct. // readFile reads the configuration from the specified file path into the provided Config struct.

View File

@@ -15,30 +15,33 @@ type ContactController struct {
EmailService *services.EmailService EmailService *services.EmailService
} }
type contactData struct { type contactData struct {
email string `validate:"required,email"` Email string `form:"email" validate:"required,email"`
name string Name string `form:"name"`
message string `validate:"required"` Message string `form:"message" validate:"required"`
Honeypot string `form:"username" validate:"eq="`
} }
func (cc *ContactController) RelayContactRequest(c *gin.Context) { func (cc *ContactController) RelayContactRequest(c *gin.Context) {
var msgData contactData var msgData contactData
if c.Query("username") != "" {
if err := c.ShouldBind(&msgData); err != nil {
// A bot is talking to us // A bot is talking to us
c.JSON(http.StatusNotAcceptable, gin.H{"error": "Not Acceptable"})
return return
} }
msgData.name = c.Query("name")
msgData.email = c.Query("email")
msgData.message = c.Query("message")
validate := validator.New() validate := validator.New()
if err := validate.Struct(msgData); err != nil { if err := validate.Struct(msgData); err != nil {
logger.Error.Printf("Couldn't validate contact form data: %v", err) logger.Error.Printf("Couldn't validate contact form data: %v", err)
c.JSON(http.StatusNotAcceptable, gin.H{"error": "Couldn't validate contact form data"}) 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) logger.Error.Printf("Couldn't send contact message mail: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't send mail"}) 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")
} }

View File

@@ -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{}),
},
}
}

View File

@@ -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)
}

View File

@@ -40,15 +40,17 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
c.JSON(http.StatusExpectationFailed, "API Key is missing") c.JSON(http.StatusExpectationFailed, "API Key is missing")
return return
} }
logger.Info.Printf("registering subscription: %+v", regData)
// Register Subscription // Register Subscription
id, err := mc.Service.RegisterSubscription(&regData.Model) id, err := mc.Service.RegisterSubscription(&regData.Model)
if err != nil { if err != nil {
logger.Error.Printf("Couldn't register Membershipmodel: %v", err) 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 return
} }
regData.Model.ID = id logger.Info.Printf("registering subscription: %+v", regData)
c.JSON(http.StatusCreated, gin.H{
"status": "success",
"id": id,
})
} }

View File

@@ -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
})),
},
}
}

View File

@@ -88,7 +88,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
} }
// Notify admin of new user registration // Notify admin of new user registration
if err := uc.EmailService.NotifyAdminOfNewUser(&regData.User); err != nil { if err := uc.EmailService.SendRegistrationNotification(&regData.User); err != nil {
logger.Error.Printf("Failed to notify admin of new user registration: %v", err) logger.Error.Printf("Failed to notify admin of new user registration: %v", err)
// Proceed without returning error since user registration is successful // 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."}) 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 return
} }
logger.Info.Printf("User: %#v", user)
uc.EmailService.SendWelcomeEmail(user) uc.EmailService.SendWelcomeEmail(user)
c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName}) c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName})

View File

@@ -1,130 +1,66 @@
package controllers package controllers
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"strconv"
"strings"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "net/url"
"path/filepath"
"regexp"
"strings"
"testing" "testing"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
smtpmock "github.com/mocktools/go-smtp-mock/v2"
"GoMembership/internal/config" "GoMembership/internal/config"
"GoMembership/internal/constants" "GoMembership/internal/constants"
"GoMembership/internal/database"
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/services"
"GoMembership/internal/utils" "GoMembership/internal/utils"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
) )
type test struct { type RegisterUserTest struct {
name string WantDBData map[string]interface{}
input string Name string
wantDBData map[string]interface{} Input string
wantResponse uint16 WantResponse int
assert bool Assert bool
} }
// type RegistrationData struct { func (rt *RegisterUserTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder) {
// User models.User `json:"user"` return GetMockedJSONContext([]byte(rt.Input), "register")
// } }
const ( func (rt *RegisterUserTest) RunHandler(c *gin.Context) {
Host = "127.0.0.1" Uc.RegisterUser(c)
Port int = 2525 }
User = "alex@mail.de"
Pass = "secret"
AdminMail = "admin@mail.de"
)
var ( func (rt *RegisterUserTest) ValidateResponse(w *httptest.ResponseRecorder) error {
uc UserController 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) { 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() tests := getTestUsers()
for _, tt := range tests { 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 { if err := runSingleTest(&tt); err != nil {
t.Errorf("Test failed: %v", err.Error()) 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 { func validateUser(assert bool, wantDBData map[string]interface{}) error {
users, err := uc.Service.GetUsers(wantDBData) users, err := Uc.Service.GetUsers(wantDBData)
if err != nil { if err != nil {
return fmt.Errorf("Error in database ops: %#v", err) 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 //check for email delivery
messages := utils.SMTPGetMessages() messages := utils.SMTPGetMessages()
for _, message := range messages { for _, message := range messages {
mail, err := utils.DecodeMail(message.MsgRequest())
if strings.Contains(message.MsgRequest(), constants.MailWelcomeSubject) { if err != nil {
if err := checkWelcomeMail(&message); err != nil { return err
}
if strings.Contains(mail.Subject, constants.MailRegistrationSubject) {
if err := checkRegistrationMail(mail, &(*users)[0]); err != nil {
return err return err
} }
} else if strings.Contains(message.MsgRequest(), constants.MailRegistrationSubject) { } else if strings.Contains(mail.Subject, constants.MailVerificationSubject) {
if err := checkRegistrationMail(&message, &(*users)[0]); err != nil { if err := checkVerificationMail(mail, &(*users)[0]); err != nil {
return err return err
} }
} else if strings.Contains(message.MsgRequest(), constants.MailVerificationSubject) { verifiedUsers, err := Uc.Service.GetUsers(wantDBData)
if err := checkVerificationMail(&message, &(*users)[0]); err != nil { if err != nil {
return err 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 { } else {
return fmt.Errorf("Subject not expected: %v", message.MsgRequest()) return fmt.Errorf("Subject not expected: %v", mail.Subject)
} }
} }
} }
return nil return nil
} }
func checkWelcomeMail(message *smtpmock.Message) error { func checkWelcomeMail(message *utils.Email, user *models.User) error {
return nil
}
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. //Check if all the relevant data has been passed to the mail.
if !strings.Contains(message.MsgRequest(), user.FirstName+" "+user.LastName) { if !strings.Contains(message.Body, user.FirstName) {
return fmt.Errorf("User first and last name(%v) has not been rendered in registration mail.", user.FirstName+" "+user.LastName) 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</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", 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</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", 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) return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
} }
if !strings.Contains(message.MsgRequest(), fmt.Sprintf("Mitgliedsnr:</strong> %v", user.Membership.ID)) { if !strings.Contains(message.Body, fmt.Sprintf("Mitgliedsnummer</strong>: %v", user.Membership.ID)) {
return fmt.Errorf("Users membership Id(%v) has not been rendered in registration mail.", 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) { if !strings.Contains(message.Body, config.BaseURL) {
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:</strong> "+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 fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.BaseURL)
} }
return nil return nil
} }
func checkVerificationMail(message *smtpmock.Message, user *models.User) error { func checkRegistrationMail(message *utils.Email, user *models.User) error {
for _, responses := range message.RcpttoRequestResponse() {
if !strings.Contains(responses[0], "RCPT TO:<"+user.Email) { if !strings.Contains(message.To, config.Recipients.UserRegistration) {
return fmt.Errorf("Registration Information didn't reach client! Recipient was: %v instead of %v", responses[0], user.Email) 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.From, config.SMTP.User) {
if !strings.Contains(message.MsgRequest(), user.Verification.VerificationToken[:len(user.Verification.VerificationToken)-1]) { return fmt.Errorf("Registration Information was sent from unexpected address! Sender was: %v instead of %v", message.From, config.SMTP.User)
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) { //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</strong>: %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</strong>: %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:</strong> %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:</strong> "+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 fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.BaseURL)
} }
return nil // open the provided link:
if err := verifyMail(verificationURL); err != 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 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 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: // 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 := getBaseUser()
user = customize(user) // Apply the customization user = customize(user) // Apply the customization
regData := RegistrationData{User: user} return &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 { func getTestUsers() []RegisterUserTest {
return []test{ return []RegisterUserTest{
{ {
name: "birthday < 18 should fail", Name: "birthday < 18 should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.DateOfBirth = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC) user.DateOfBirth = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
return user return user
}), })),
}, },
{ {
name: "FirstName empty, should fail", Name: "FirstName empty, should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.FirstName = "" user.FirstName = ""
return user return user
}), })),
}, },
{ {
name: "LastName Empty should fail", Name: "LastName Empty should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.LastName = "" user.LastName = ""
return user return user
}), })),
}, },
{ {
name: "EMail wrong format should fail", Name: "EMail wrong format should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "johnexample.com"}, WantDBData: map[string]interface{}{"email": "johnexample.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "johnexample.com" user.Email = "johnexample.com"
return user return user
}), })),
}, },
{ {
name: "Missing Zip Code should fail", Name: "Missing Zip Code should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.ZipCode = "" user.ZipCode = ""
return user return user
}), })),
}, },
{ {
name: "Missing Address should fail", Name: "Missing Address should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Address = "" user.Address = ""
return user return user
}), })),
}, },
{ {
name: "Missing City should fail", Name: "Missing City should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.City = "" user.City = ""
return user return user
}), })),
}, },
{ {
name: "Missing IBAN should fail", Name: "Missing IBAN should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = "" user.BankAccount.IBAN = ""
return user return user
}), })),
}, },
{ {
name: "Invalid IBAN should fail", Name: "Invalid IBAN should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = "DE1234234123134" user.BankAccount.IBAN = "DE1234234123134"
return user return user
}), })),
}, },
{ {
name: "Missing subscription plan should fail", Name: "Missing subscription plan should fail",
wantResponse: http.StatusNotAcceptable, WantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "" user.Membership.SubscriptionModel.Name = ""
return user return user
}), })),
}, },
{ {
name: "Invalid subscription plan should fail", Name: "Invalid subscription plan should fail",
wantResponse: http.StatusNotFound, WantResponse: http.StatusNotFound,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN" user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN"
return user return user
}), })),
}, },
{ {
name: "Correct Entry should pass", Name: "Correct Entry should pass",
wantResponse: http.StatusCreated, WantResponse: http.StatusCreated,
wantDBData: map[string]interface{}{"Email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"Email": "john.doe@example.com"},
assert: true, Assert: true,
input: generateInputJSON(func(user models.User) models.User { return user }), Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { return user })),
}, },
{ {
name: "Email duplicate should fail", Name: "Email duplicate should fail",
wantResponse: http.StatusConflict, WantResponse: http.StatusConflict,
wantDBData: map[string]interface{}{"first_name": "Jane"}, WantDBData: map[string]interface{}{"first_name": "Jane"},
assert: false, Assert: false,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.FirstName = "Jane" user.FirstName = "Jane"
return user return user
}), })),
}, },
{ {
name: "Company present should pass", Name: "Company present should pass",
wantResponse: http.StatusCreated, WantResponse: http.StatusCreated,
wantDBData: map[string]interface{}{"Email": "john.doe2@example.com"}, WantDBData: map[string]interface{}{"Email": "john.doe2@example.com"},
assert: true, Assert: true,
input: generateInputJSON(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.doe2@example.com" user.Email = "john.doe2@example.com"
user.Company = "ACME" user.Company = "ACME"
return user return user
}), })),
}, },
} }
} }

View File

@@ -3,6 +3,8 @@ package repositories
import ( import (
"GoMembership/internal/database" "GoMembership/internal/database"
"gorm.io/gorm"
"GoMembership/internal/models" "GoMembership/internal/models"
) )
@@ -10,6 +12,7 @@ type SubscriptionModelsRepositoryInterface interface {
CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (int64, error) CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (int64, error)
GetMembershipModelNames() ([]string, error) GetMembershipModelNames() ([]string, error)
GetModelByName(modelname *string) (*models.SubscriptionModel, error) GetModelByName(modelname *string) (*models.SubscriptionModel, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
} }
type SubscriptionModelsRepository struct{} type SubscriptionModelsRepository struct{}
@@ -38,3 +41,15 @@ func (sr *SubscriptionModelsRepository) GetMembershipModelNames() ([]string, err
} }
return names, nil 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
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "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.GET("/backend/verify", userController.VerifyMailHandler)
router.POST("/backend/api/register", userController.RegisterUser) router.POST("/backend/api/register", userController.RegisterUser)
router.POST("/backend/api/register/subscription", membershipcontroller.RegisterSubscription) router.POST("/backend/api/register/subscription", membershipcontroller.RegisterSubscription)

View File

@@ -30,7 +30,7 @@ func Run() {
logger.Error.Fatalf("Couldn't init database: %v", err) 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{} var consentRepo repositories.ConsentRepositoryInterface = &repositories.ConsentRepository{}
consentService := &services.ConsentService{Repo: consentRepo} consentService := &services.ConsentService{Repo: consentRepo}
@@ -47,6 +47,7 @@ func Run() {
userController := &controllers.UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService} userController := &controllers.UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
membershipController := &controllers.MembershipController{Service: *membershipService} membershipController := &controllers.MembershipController{Service: *membershipService}
contactController := &controllers.ContactController{EmailService: emailService}
router := gin.Default() router := gin.Default()
// gin.SetMode(gin.ReleaseMode) // gin.SetMode(gin.ReleaseMode)
router.Static(config.Templates.StaticPath, "./style") router.Static(config.Templates.StaticPath, "./style")
@@ -55,7 +56,7 @@ func Run() {
router.Use(gin.Logger()) router.Use(gin.Logger())
// router.Use(middlewares.LoggerMiddleware()) // router.Use(middlewares.LoggerMiddleware())
routes.RegisterRoutes(router, userController, membershipController) routes.RegisterRoutes(router, userController, membershipController, contactController)
// create subrouter for teh authenticated area /account // create subrouter for teh authenticated area /account
// also pthprefix matches everything below /account // also pthprefix matches everything below /account
// accountRouter := router.PathPrefix("/account").Subrouter() // accountRouter := router.PathPrefix("/account").Subrouter()

View File

@@ -13,13 +13,12 @@ import (
) )
type EmailService struct { type EmailService struct {
dialer *gomail.Dialer dialer *gomail.Dialer
adminEmail string
} }
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) 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 { 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 Company string
FirstName string FirstName string
MembershipModel string MembershipModel string
BASEURL string
MembershipID int64 MembershipID int64
MembershipFee float32 MembershipFee float32
RentalFee float32 RentalFee float32
BASEURL string
}{ }{
Company: user.Company, Company: user.Company,
FirstName: user.FirstName, FirstName: user.FirstName,
@@ -110,24 +109,24 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
return s.SendEmail(user.Email, subject, body) 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 // Prepare data to be injected into the template
data := struct { data := struct {
City string FirstName string
Email string DateOfBirth string
LastName string LastName string
MembershipModel string MembershipModel string
Address string Address string
IBAN string IBAN string
FirstName string Email string
Phone string Phone string
DateOfBirth string City string
Company string Company string
ZipCode string ZipCode string
BASEURL string
MembershipID int64 MembershipID int64
RentalFee float32 RentalFee float32
MembershipFee float32 MembershipFee float32
BASEURL string
}{ }{
Company: user.Company, Company: user.Company,
FirstName: user.FirstName, FirstName: user.FirstName,
@@ -152,7 +151,7 @@ func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error {
logger.Error.Print("Couldn't send admin notification mail") logger.Error.Print("Couldn't send admin notification mail")
return err 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 { 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") logger.Error.Print("Couldn't send contact form message mail")
return err return err
} }
return s.SendEmail(config.SMTP.AdminEmail, subject, body) return s.SendEmail(config.Recipients.ContactForm, subject, body)
} }

View File

@@ -1,11 +1,15 @@
package services package services
import ( import (
"net/http"
"slices"
"time"
"github.com/go-playground/validator/v10"
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"slices"
"time"
) )
type MembershipServiceInterface interface { type MembershipServiceInterface interface {
@@ -14,6 +18,7 @@ type MembershipServiceInterface interface {
RegisterSubscription(subscription *models.SubscriptionModel) (int64, error) RegisterSubscription(subscription *models.SubscriptionModel) (int64, error)
GetMembershipModelNames() ([]string, error) GetMembershipModelNames() ([]string, error)
GetModelByName(modelname *string) (*models.SubscriptionModel, error) GetModelByName(modelname *string) (*models.SubscriptionModel, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
} }
type MembershipService struct { type MembershipService struct {
@@ -32,6 +37,9 @@ func (service *MembershipService) FindMembershipByUserID(userID int64) (*models.
// Membership_Subscriptions // Membership_Subscriptions
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (int64, error) { func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (int64, error) {
if err := validateSubscriptionData(subscription); err != nil {
return http.StatusNotAcceptable, err
}
return service.SubscriptionRepo.CreateSubscriptionModel(subscription) return service.SubscriptionRepo.CreateSubscriptionModel(subscription)
} }
@@ -50,3 +58,15 @@ func (service *MembershipService) GetModelByName(modelname *string) (*models.Sub
} }
return service.SubscriptionRepo.GetModelByName(modelname) 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)
}

View File

@@ -34,8 +34,7 @@ func (service *UserService) RegisterUser(user *models.User) (int64, string, erro
} }
user.Salt = base64.StdEncoding.EncodeToString(salt) user.Salt = base64.StdEncoding.EncodeToString(salt)
*/ */
err := validateRegistrationData(user) if err := validateRegistrationData(user); err != nil {
if err != nil {
return http.StatusNotAcceptable, "", err return http.StatusNotAcceptable, "", err
} }

View File

@@ -1,10 +1,26 @@
package utils package utils
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"encoding/base64" "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) { func GenerateRandomString(length int) (string, error) {
bytes := make([]byte, length) bytes := make([]byte, length)
_, err := rand.Read(bytes) _, err := rand.Read(bytes)
@@ -17,3 +33,70 @@ func GenerateRandomString(length int) (string, error) {
func GenerateVerificationToken() (string, error) { func GenerateVerificationToken() (string, error) {
return GenerateRandomString(32) 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())
}

View File

@@ -55,7 +55,7 @@
Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur
erfolgreichen E-Mail-Verifikation und willkommen in unserem erfolgreichen E-Mail-Verifikation und willkommen in unserem
Verein! Verein!
</div> </div>
<div <div
style=" style="
font-size: 22px; font-size: 22px;