diff --git a/go.mod b/go.mod index 8d5ed75..c617595 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.22.2 require ( github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/jbub/banking v0.8.0 + github.com/stretchr/testify v1.9.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.10 @@ -14,13 +17,12 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // in github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/goccy/go-json v0.10.3 // indirect - github.com/jbub/banking v0.8.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -31,6 +33,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/internal/controllers/user_controller_test.go b/internal/controllers/user_controller_test.go new file mode 100644 index 0000000..1e732cc --- /dev/null +++ b/internal/controllers/user_controller_test.go @@ -0,0 +1,277 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "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/pkg/logger" +) + +type keyValuePair struct { + value interface{} + key string +} + +type test struct { + name string + wantResponse uint16 + userEmail string + wantDBData []keyValuePair + assert bool + input string +} + +func TestUserController(t *testing.T) { + err := database.InitDB("test.db") + if err != nil { + t.Errorf("Failed to create DB: %#v", err) + } + + cfg := config.LoadConfig() + emailService := services.NewEmailService(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.User, cfg.SMTP.Password, cfg.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 { + 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) + } + }) + } + if err := deleteTestDB("test.db"); err != nil { + t.Errorf("Failed to tear down DB: %#v", err) + } +} + +func initSubscriptionPlans() error { + subscription := models.SubscriptionModel{ + Name: "basic", + } + 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) + c.Request, _ = http.NewRequest("POST", "/register", bytes.NewBuffer(jsonStr)) + 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 { + return err + } + return nil +} + +// TEST DATA: +func generateUserJSON(user models.User) string { + data, err := json.Marshal(user) + if err != nil { + logger.Error.Printf("couldn't generate Json from Uer: %#v\nERROR: %#v", user, err) + return "" + } + return string(data) +} + +func getTestUsers() []test { + return []test{ + { + name: "birthday < 18 should fail", + wantResponse: http.StatusNotAcceptable, + wantDBData: []keyValuePair{ + { + key: "Email", + value: "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, + }), + }, + { + name: "FirstName empty, should fail", + wantResponse: http.StatusNotAcceptable, + wantDBData: []keyValuePair{ + { + key: "Email", + value: "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, + }), + }, + /* 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, + }, + { + 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, + }, */ + } +} diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go deleted file mode 100644 index 0a45e89..0000000 --- a/internal/handlers/user_handler.go +++ /dev/null @@ -1,11 +0,0 @@ -package handlers - -import ( - "net/http" - "GoMembership/internal/controllers" -) - -func RegisterUserHandler(controller *controllers.UserController) http.Handler { - return http.HandlerFunc(controller.RegisterUser) -} - diff --git a/internal/models/user.go b/internal/models/user.go index b5bb9fd..210e36a 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -11,6 +11,7 @@ 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"` Phone string `json:"phone" validate:"omitempty,omitnil"` Notes *string `json:"notes"` FirstName string `gorm:"not null" json:"first_name" validate:"required"`