add: mockSMTP Server for testing; getUser func

This commit is contained in:
$(pass /github/name)
2024-08-08 09:29:49 +02:00
parent 602ac0fef0
commit c02b96e538
9 changed files with 321 additions and 187 deletions

2
go.mod
View File

@@ -13,6 +13,8 @@ require (
require github.com/kelseyhightower/envconfig v1.4.0
require github.com/mocktools/go-smtp-mock/v2 v2.3.1 // indirect
require (
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect

2
go.sum
View File

@@ -48,6 +48,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM=
github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -34,20 +34,25 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode userdata"})
return
}
if regData.User.Membership.SubscriptionModel.Name == "" {
logger.Error.Printf("No subscription model provided")
c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"})
return
}
selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name)
if err != nil {
logger.Error.Printf("No subscription model found: %#v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Not a valid subscription model"})
c.JSON(http.StatusNotFound, gin.H{"error": "Not a valid subscription model"})
return
}
regData.User.Membership.SubscriptionModel = *selectedModel
logger.Info.Printf("REGISTERING user: %#v", regData.User)
// logger.Info.Printf("REGISTERING user: %#v", regData.User)
// Register User
id, token, err := uc.Service.RegisterUser(&regData.User)
if err != nil {
logger.Error.Printf("Couldn't register User: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Couldn't register User: %v", err)})
c.JSON(int(id), gin.H{"error": fmt.Sprintf("Couldn't register User: %v", err)})
return
}
regData.User.ID = id

View File

@@ -3,7 +3,9 @@ package controllers
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
// "io/ioutil"
"net/http"
"net/http/httptest"
"os"
@@ -12,37 +14,64 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"reflect"
"GoMembership/internal/config"
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/services"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
)
type keyValuePair struct {
value interface{}
key string
}
type test struct {
name string
wantResponse uint16
userEmail string
wantDBData []keyValuePair
assert bool
input string
wantDBData map[string]interface{}
wantResponse uint16
assert bool
}
// type RegistrationData struct {
// User models.User `json:"user"`
// }
const (
Host = "127.0.0.1"
Port int = 2525
User = "alex@mail.de"
Pass = "secret"
AdminMail = "admin@mail.de"
)
var (
uc UserController
)
func TestUserController(t *testing.T) {
err := database.InitDB("test.db")
if err != nil {
_ = 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}
@@ -57,44 +86,16 @@ func TestUserController(t *testing.T) {
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
userService := &services.UserService{Repo: userRepo}
uc := UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
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 {
c, _ := getMockedContext([]byte(tt.input))
t.Run(tt.name, func(t *testing.T) {
uc.RegisterUser(c)
user, err := userRepo.FindUserByEmail(tt.userEmail)
if err == gorm.ErrRecordNotFound && !tt.assert {
//pass
} else if err == nil && tt.assert {
// check value:
if tt.wantDBData != nil {
for _, kvp := range tt.wantDBData {
/* dbValue, err := getFieldValue(*user, kvp.key)
if err != nil {
t.Errorf("getFieldValue failed: %#v", err)
} */
dbValue, err := getAttr(&user, kvp.key)
if err != nil {
t.Errorf("Couldn't get Attribute: %#v", err)
}
if kvp.value != dbValue {
// fail
t.Errorf("Value is not expected: %v != %v", kvp.value, dbValue)
}
}
}
} else {
// fail
t.Errorf("FindUserByEmail failed: %#v", err)
}
runSingleTest(t, &tt)
})
}
if err := deleteTestDB("test.db"); err != nil {
@@ -102,9 +103,43 @@ func TestUserController(t *testing.T) {
}
}
func runSingleTest(t *testing.T, tt *test) {
c, w := getMockedContext([]byte(tt.input))
uc.RegisterUser(c)
if w.Code != int(tt.wantResponse) {
t.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, tt.wantResponse)
}
validateUser(t, tt.assert, tt.wantDBData)
}
func handleError(t *testing.T, err error, assert bool) {
if err == gorm.ErrRecordNotFound && !assert {
return // Expected case: user not found and assertion is false
}
if err != nil {
t.Errorf("Error during testing: %#v", err)
}
}
func validateUser(t *testing.T, assert bool, wantDBData map[string]interface{}) {
users, err := uc.Service.GetUsers(wantDBData)
if err != nil {
t.Errorf("Error in database ops: %#v", err)
}
if assert != (len(*users) != 0) {
t.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users)
}
}
func initSubscriptionPlans() error {
subscription := models.SubscriptionModel{
Name: "basic",
Name: "Basic",
Details: "Test Plan",
MonthlyFee: 2,
HourlyRate: 3,
}
result := database.DB.Create(&subscription)
if result.Error != nil {
@@ -117,39 +152,16 @@ func getMockedContext(jsonStr []byte) (*gin.Context, *httptest.ResponseRecorder)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/register", bytes.NewBuffer(jsonStr))
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 getAttr(obj interface{}, fieldName string) (interface{}, error) {
pointToStruct := reflect.ValueOf(obj) // addressable
curStruct := pointToStruct.Elem()
if curStruct.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected a struct, but got %v", curStruct.Kind())
}
curField := curStruct.FieldByName(fieldName) // type: reflect.Value
if !curField.IsValid() {
return nil, fmt.Errorf("no such field: %s in user", fieldName)
}
return curField.Interface(), nil
}
/* func getFieldValue(user models.User, fieldName string) (interface{}, error) {
v := reflect.ValueOf(user)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected a struct, but got %v", v.Kind())
}
fieldVal := v.FieldByName(fieldName)
if !fieldVal.IsValid() {
return nil, fmt.Errorf("no such field: %s in user", fieldName)
}
return fieldVal.Interface(), nil
} */
func deleteTestDB(dbPath string) error {
err := os.Remove(dbPath)
if err != nil {
@@ -159,13 +171,35 @@ func deleteTestDB(dbPath string) error {
}
// TEST DATA:
func generateUserJSON(user models.User) string {
data, err := json.Marshal(user)
func getBaseUser() models.User {
return models.User{
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Address: "Pablo Escobar Str. 4",
ZipCode: "25474",
City: "Hasloh",
Phone: "01738484993",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
ProfilePicture: "",
Password: "password123",
Company: "",
}
}
func generateInputJSON(customize func(models.User) models.User) string {
user := getBaseUser()
user = customize(user) // Apply the customization
regData := RegistrationData{User: user}
jsonBytes, err := json.Marshal(regData)
if err != nil {
logger.Error.Printf("couldn't generate Json from Uer: %#v\nERROR: %#v", user, err)
logger.Error.Printf("couldn't generate Json from User: %#v\nERROR: %#v", regData, err)
return ""
}
return string(data)
return string(jsonBytes)
}
func getTestUsers() []test {
@@ -173,105 +207,129 @@ func getTestUsers() []test {
{
name: "birthday < 18 should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: []keyValuePair{
{
key: "Email",
value: "john.doe@example.com"},
},
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateUserJSON(
models.User{
DateOfBirth: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Address: "123 Main St",
ZipCode: "12345",
City: "Cityville",
Phone: "123-456-7890",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
ProfilePicture: "http://example.com/profile.jpg",
Company: nil,
Notes: nil,
Password: "password123",
ID: 1,
PaymentStatus: 1,
Status: 1,
RoleID: 1,
input: generateInputJSON(func(user models.User) models.User {
user.DateOfBirth = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
return user
}),
},
{
name: "FirstName empty, should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: []keyValuePair{
{
key: "Email",
value: "john.doe@example.com"},
},
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateUserJSON(
models.User{
DateOfBirth: time.Date(1990, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "",
LastName: "Doe",
Email: "john.doe@example.com",
Address: "123 Main St",
ZipCode: "12345",
City: "Cityville",
Phone: "123-456-7890",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
ProfilePicture: "http://example.com/profile.jpg",
Company: nil,
Notes: nil,
Password: "password123",
ID: 1,
PaymentStatus: 1,
Status: 1,
RoleID: 1,
input: generateInputJSON(func(user models.User) models.User {
user.FirstName = ""
return user
}),
},
/* Vjjjjjjjj
models.User {
DateOfBirth: time.Date(1985, time.March, 15, 0, 0, 0, 0, time.UTC),
FirstName: "Jane",
LastName: "Smith",
Email: "jane.smith@example.com",
Address: "456 Oak St",
ZipCode: "67890",
City: "Townsville",
Phone: "098-765-4321",
BankAccount: models.BankAccount{IBAN: "FR7630006000011234567890189"},
Membership: models.Membership{SubscriptionModel: SubscriptionModel{Name: "Premium", Details: "Premium Subscription", Conditions: "None", MonthlyFee: 19.99, HourlyRate: 25.00}},
ProfilePicture: "http://example.com/profile2.jpg",
Company: stringPtr("ExampleCorp"),
Notes: stringPtr("Important client"),
Password: "securepassword456",
ID: 2,
PaymentStatus: 0,
Status: 2,
RoleID: 2,
{
name: "LastName Empty should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.LastName = ""
return user
}),
},
{
DateOfBirth: time.Date(2000, time.July, 30, 0, 0, 0, 0, time.UTC),
FirstName: "Alice",
LastName: "Brown",
Email: "alice.brown@example.com",
Address: "789 Pine St",
ZipCode: "11223",
City: "Villageville",
Phone: "555-555-5555",
BankAccount: models.BankAccount{IBAN: "GB29NWBK60161331926819"},
Membership: models.Membership{SubscriptionModel: SubscriptionModel{Name: "Standard", Details: "Standard Subscription", Conditions: "None", MonthlyFee: 14.99, HourlyRate: 20.00}},
ProfilePicture: "",
Company: stringPtr("AnotherCorp"),
Notes: nil,
Password: "mypassword789",
ID: 3,
PaymentStatus: 1,
Status: 1,
RoleID: 3,
}, */
name: "EMail wrong format should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "johnexample.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.Email = "johnexample.com"
return user
}),
},
{
name: "Missing Zip Code should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.ZipCode = ""
return user
}),
},
{
name: "Missing Address should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.Address = ""
return user
}),
},
{
name: "Missing City should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.City = ""
return user
}),
},
{
name: "Missing IBAN should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.BankAccount.IBAN = ""
return user
}),
},
{
name: "Invalid IBAN should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.BankAccount.IBAN = "DE1234234123134"
return user
}),
},
{
name: "Missing subscription plan should fail",
wantResponse: http.StatusNotAcceptable,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = ""
return user
}),
},
{
name: "Invalid subscription plan should fail",
wantResponse: http.StatusNotFound,
wantDBData: map[string]interface{}{"email": "john.doe@example.com"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN"
return user
}),
},
{
name: "Correct Entry should pass",
wantResponse: http.StatusCreated,
wantDBData: map[string]interface{}{"Email": "john.doe@example.com"},
assert: true,
input: generateInputJSON(func(user models.User) models.User { return user }),
},
{
name: "Email duplicate should fail",
wantResponse: http.StatusConflict,
wantDBData: map[string]interface{}{"first_name": "Jane"},
assert: false,
input: generateInputJSON(func(user models.User) models.User {
user.FirstName = "Jane"
return user
}),
},
}
}

View File

@@ -11,14 +11,14 @@ type User struct {
DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" validate:"required,age"`
CreatedAt time.Time
Salt *string `json:"-"`
Company *string `json:"company" validate:"omitempty,omitnil"`
Company string `json:"company" validate:"omitempty,omitnil"`
Phone string `json:"phone" validate:"omitempty,omitnil"`
Notes *string `json:"notes"`
FirstName string `gorm:"not null" json:"first_name" validate:"required"`
Password string `json:"password"`
Email string `gorm:"unique;not null" json:"email" validate:"required,email"`
LastName string `gorm:"not null" json:"last_name" validate:"required"`
ProfilePicture string `json:"profile_picture" validate:"omitempty,image"`
ProfilePicture string `json:"profile_picture" validate:"omitempty,omitnil,image"`
Address string `gorm:"not null" json:"address" validate:"required"`
ZipCode string `gorm:"not null" json:"zip_code" validate:"required,alphanum"`
City string `form:"not null" json:"city" validate:"required,alphaunicode"`

View File

@@ -17,6 +17,7 @@ import (
type UserRepositoryInterface interface {
CreateUser(user *models.User) (int64, error)
UpdateUser(userID int64, user *models.User) error
GetUsers(where map[string]interface{}) (*[]models.User, error)
FindUserByID(id int64) (*models.User, error)
FindUserByEmail(email string) (*models.User, error)
SetVerificationToken(user *models.User, token *string) (int64, error)
@@ -51,6 +52,24 @@ func (ur *UserRepository) UpdateUser(userID int64, user *models.User) error {
return nil
}
func (ur *UserRepository) GetUsers(where map[string]interface{}) (*[]models.User, error) {
var users []models.User
result := database.DB.
Preload("Consents").
Preload("BankAccount").
Preload("Verification").
Preload("Membership", func(db *gorm.DB) *gorm.DB {
return db.Preload("SubscriptionModel")
}).Where(where).Find(&users)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &users, nil
}
func (ur *UserRepository) FindUserByID(id int64) (*models.User, error) {
var user models.User
result := database.DB.

View File

@@ -86,7 +86,7 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
MembershipFee float32
RentalFee float32
}{
Company: *user.Company,
Company: user.Company,
FirstName: user.FirstName,
MembershipModel: user.Membership.SubscriptionModel.Name,
MembershipID: user.Membership.ID,
@@ -121,7 +121,7 @@ func (s *EmailService) NotifyAdminOfNewUser(user *models.User) error {
RentalFee float32
MembershipFee float32
}{
Company: *user.Company,
Company: user.Company,
FirstName: user.FirstName,
LastName: user.LastName,
MembershipModel: user.Membership.SubscriptionModel.Name,

View File

@@ -1,20 +1,24 @@
package services
import (
"net/http"
"strings"
"github.com/go-playground/validator/v10"
"GoMembership/internal/constants"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10"
// "crypto/rand"
// "encoding/base64"
// "golang.org/x/crypto/bcrypt"
"time"
)
type UserServiceInterface interface {
RegisterUser(user *models.User) (int64, string, error)
FindUserByEmail(email string) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error)
// AuthenticateUser(email, password string) (*models.User, error)A
VerifyUser(token *string) (*models.User, error)
}
@@ -32,7 +36,7 @@ func (service *UserService) RegisterUser(user *models.User) (int64, string, erro
*/
err := validateRegistrationData(user)
if err != nil {
return -1, "", err
return http.StatusNotAcceptable, "", err
}
user.Status = constants.UnverifiedStatus
@@ -40,27 +44,38 @@ func (service *UserService) RegisterUser(user *models.User) (int64, string, erro
user.UpdatedAt = time.Now()
id, err := service.Repo.CreateUser(user)
if err != nil {
return -1, "", err
if err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") {
return http.StatusConflict, "", err
} else if err != nil {
return http.StatusInternalServerError, "", err
}
user.ID = id
token, err := utils.GenerateVerificationToken()
if err != nil {
return -1, "", err
return http.StatusInternalServerError, "", err
}
logger.Info.Printf("TOKEN: %v", token)
_, err = service.Repo.SetVerificationToken(user, &token)
if err != nil {
return -1, "", err
return http.StatusInternalServerError, "", err
}
return id, token, nil
}
func (service *UserService) FindUserByEmail(email string) (*models.User, error) {
return service.Repo.FindUserByEmail(email)
}
func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) {
return service.Repo.GetUsers(where)
}
func (service *UserService) VerifyUser(token *string) (*models.User, error) {
user, err := service.Repo.VerifyUserOfToken(token)
if err != nil {

View File

@@ -0,0 +1,33 @@
package utils
import (
smtpmock "github.com/mocktools/go-smtp-mock/v2"
)
var Server smtpmock.Server
// StartMockSMTPServer starts a mock SMTP server for testing
func SMTPStart(host string, port int) error {
Server = *smtpmock.New(smtpmock.ConfigurationAttr{
HostAddress: host,
PortNumber: port,
LogToStdout: false,
LogServerActivity: false,
})
if err := Server.Start(); err != nil {
return err
}
return nil
}
func SMTPGetMessages() []smtpmock.Message {
return Server.MessagesAndPurge()
}
func SMTPStop() error {
if err := Server.Stop(); err != nil {
return err
}
return nil
}