backend moved to separate directory
backend: deleted the old structure
This commit is contained in:
71
go-backend/internal/controllers/SQLInjection_test.go
Normal file
71
go-backend/internal/controllers/SQLInjection_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SQLInjectionTest struct {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
expectedStatus int
|
||||
}
|
||||
|
||||
func (sit *SQLInjectionTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
|
||||
loginData := loginInput{
|
||||
Email: sit.email,
|
||||
Password: sit.password,
|
||||
}
|
||||
jsonData, _ := json.Marshal(loginData)
|
||||
return GetMockedJSONContext(jsonData, "/login")
|
||||
}
|
||||
|
||||
func (sit *SQLInjectionTest) RunHandler(c *gin.Context, router *gin.Engine) {
|
||||
router.POST("/login", Uc.LoginHandler)
|
||||
router.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func (sit *SQLInjectionTest) ValidateResponse(w *httptest.ResponseRecorder) error {
|
||||
if sit.expectedStatus != w.Code {
|
||||
responseBody, _ := io.ReadAll(w.Body)
|
||||
return fmt.Errorf("SQL Injection Attempt: Didn't get the expected response code: got: %v; expected: %v. Context: %#v", w.Code, sit.expectedStatus, string(responseBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sit *SQLInjectionTest) ValidateResult() error {
|
||||
// Add any additional validation if needed
|
||||
return nil
|
||||
}
|
||||
|
||||
func testSQLInjectionAttempt(t *testing.T) {
|
||||
tests := []SQLInjectionTest{
|
||||
{
|
||||
name: "SQL Injection Attempt in Email",
|
||||
email: "' OR '1'='1",
|
||||
password: "password123",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "SQL Injection Attempt in Password",
|
||||
email: "user@example.com",
|
||||
password: "' OR '1'='1",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
go-backend/internal/controllers/XSS_test.go
Normal file
31
go-backend/internal/controllers/XSS_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testXSSAttempt(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/register", Uc.RegisterUser)
|
||||
|
||||
xssPayload := "<script>alert('XSS')</script>"
|
||||
user := getBaseUser()
|
||||
user.FirstName = xssPayload
|
||||
user.Email = "user@xss.hack"
|
||||
jsonData, _ := json.Marshal(RegistrationData{User: user})
|
||||
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.NotContains(t, w.Body.String(), xssPayload)
|
||||
}
|
||||
50
go-backend/internal/controllers/contactController.go
Normal file
50
go-backend/internal/controllers/contactController.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type ContactController struct {
|
||||
EmailService *services.EmailService
|
||||
}
|
||||
type contactData struct {
|
||||
Email string `form:"REPLY_TO" 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 err := c.ShouldBind(&msgData); err != nil {
|
||||
// A bot is talking to us
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": "Not Acceptable"})
|
||||
return
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
if err := validate.Struct(msgData); err != nil {
|
||||
logger.Error.Printf("Couldn't validate contact form data: %#v: %v", msgData, err)
|
||||
c.HTML(http.StatusNotAcceptable, "contactForm_reply.html", gin.H{"Error": "Form validation failed. Please check again."})
|
||||
// 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 {
|
||||
logger.Error.Printf("Couldn't send contact message mail: %v", err)
|
||||
c.HTML(http.StatusInternalServerError, "contactForm_reply.html", gin.H{"Error": "Email submission failed. Please try again."})
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't send mail"})
|
||||
return
|
||||
}
|
||||
|
||||
// c.JSON(http.StatusAccepted, "Your message has been sent")
|
||||
c.HTML(http.StatusAccepted, "contactForm_reply.html", gin.H{"Success": true})
|
||||
}
|
||||
159
go-backend/internal/controllers/contactController_test.go
Normal file
159
go-backend/internal/controllers/contactController_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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, *gin.Engine) {
|
||||
return GetMockedFormContext(rt.Input, "/contact")
|
||||
}
|
||||
|
||||
func (rt *RelayContactRequestTest) RunHandler(c *gin.Context, router *gin.Engine) {
|
||||
router.POST("/contact", Cc.RelayContactRequest)
|
||||
router.ServeHTTP(c.Writer, c.Request)
|
||||
// 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"},
|
||||
"REPLY_TO": {"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{
|
||||
"REPLY_TO": "",
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "mail invalid, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
Assert: false,
|
||||
Input: *customizeRequest(
|
||||
map[string]string{
|
||||
"REPLY_TO": "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{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
313
go-backend/internal/controllers/controllers_test.go
Normal file
313
go-backend/internal/controllers/controllers_test.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"GoMembership/internal/config"
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/database"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/internal/validation"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type TestCase interface {
|
||||
SetupContext() (*gin.Context, *httptest.ResponseRecorder, *gin.Engine)
|
||||
RunHandler(*gin.Context, *gin.Engine)
|
||||
ValidateResponse(*httptest.ResponseRecorder) error
|
||||
ValidateResult() error
|
||||
}
|
||||
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
Port int = 2525
|
||||
)
|
||||
|
||||
type loginInput struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
Uc *UserController
|
||||
Mc *MembershipController
|
||||
Cc *ContactController
|
||||
)
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
_ = deleteTestDB("test.db")
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current working directory: %v", err)
|
||||
}
|
||||
|
||||
// Build paths relative to the current working directory
|
||||
configFilePath := filepath.Join(cwd, "..", "..", "configs", "config.json")
|
||||
templateHTMLPath := filepath.Join(cwd, "..", "..", "templates", "html")
|
||||
templateMailPath := filepath.Join(cwd, "..", "..", "templates", "email")
|
||||
|
||||
if err := os.Setenv("TEMPLATE_MAIL_PATH", templateMailPath); err != nil {
|
||||
log.Fatalf("Error setting environment variable: %v", err)
|
||||
}
|
||||
if err := os.Setenv("TEMPLATE_HTML_PATH", templateHTMLPath); err != nil {
|
||||
log.Fatalf("Error setting environment variable: %v", err)
|
||||
}
|
||||
if err := os.Setenv("CONFIG_FILE_PATH", configFilePath); err != nil {
|
||||
log.Fatalf("Error setting environment variable: %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)
|
||||
}
|
||||
if err := os.Setenv("DB_PATH", "test.db"); err != nil {
|
||||
log.Fatalf("Error setting environment variable: %v", err)
|
||||
}
|
||||
config.LoadConfig()
|
||||
if err := database.Open("test.db", config.Recipients.AdminEmail); err != nil {
|
||||
log.Fatalf("Failed to create DB: %#v", err)
|
||||
}
|
||||
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 licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
|
||||
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
|
||||
userService := &services.UserService{Repo: userRepo, Licences: licenceRepo}
|
||||
|
||||
licenceService := &services.LicenceService{Repo: licenceRepo}
|
||||
|
||||
Uc = &UserController{Service: userService, LicenceService: licenceService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
|
||||
Mc = &MembershipController{UserController: &MockUserController{}, Service: *membershipService}
|
||||
Cc = &ContactController{EmailService: emailService}
|
||||
|
||||
if err := initSubscriptionPlans(); err != nil {
|
||||
log.Fatalf("Failed to init Subscription plans: %#v", err)
|
||||
}
|
||||
|
||||
if err := initLicenceCategories(); err != nil {
|
||||
log.Fatalf("Failed to init Categories: %v", err)
|
||||
}
|
||||
admin := models.User{
|
||||
FirstName: "Ad",
|
||||
LastName: "min",
|
||||
Email: "admin@example.com",
|
||||
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Company: "SampleCorp",
|
||||
Phone: "+123456789",
|
||||
Address: "123 Main Street",
|
||||
ZipCode: "12345",
|
||||
City: "SampleCity",
|
||||
Status: constants.ActiveStatus,
|
||||
RoleID: 8,
|
||||
}
|
||||
admin.SetPassword("securepassword")
|
||||
database.DB.Create(&admin)
|
||||
validation.SetupValidators()
|
||||
t.Run("userController", func(t *testing.T) {
|
||||
testUserController(t)
|
||||
})
|
||||
|
||||
t.Run("SQL_Injection", func(t *testing.T) {
|
||||
testSQLInjectionAttempt(t)
|
||||
})
|
||||
|
||||
t.Run("contactController", func(t *testing.T) {
|
||||
testContactController(t)
|
||||
})
|
||||
|
||||
t.Run("membershipController", func(t *testing.T) {
|
||||
testMembershipController(t)
|
||||
})
|
||||
|
||||
t.Run("XSSAttempt", func(t *testing.T) {
|
||||
testXSSAttempt(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 initLicenceCategories() error {
|
||||
categories := []models.Category{
|
||||
{Name: "AM"},
|
||||
{Name: "A1"},
|
||||
{Name: "A2"},
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
{Name: "C1"},
|
||||
{Name: "C"},
|
||||
{Name: "D1"},
|
||||
{Name: "D"},
|
||||
{Name: "BE"},
|
||||
{Name: "C1E"},
|
||||
{Name: "CE"},
|
||||
{Name: "D1E"},
|
||||
{Name: "DE"},
|
||||
{Name: "T"},
|
||||
{Name: "L"},
|
||||
}
|
||||
for _, category := range categories {
|
||||
result := database.DB.Create(&category)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSubscriptionPlans() error {
|
||||
subscriptions := []models.SubscriptionModel{
|
||||
{
|
||||
Name: "Basic",
|
||||
Details: "Test Plan",
|
||||
MonthlyFee: 2,
|
||||
HourlyRate: 3,
|
||||
},
|
||||
{
|
||||
Name: "additional",
|
||||
Details: "This plan needs another membership id to validate",
|
||||
RequiredMembershipField: "ParentMembershipID",
|
||||
MonthlyFee: 2,
|
||||
HourlyRate: 3,
|
||||
},
|
||||
}
|
||||
for _, subscription := range subscriptions {
|
||||
|
||||
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.Engine) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
router := gin.New()
|
||||
|
||||
// Load HTML templates
|
||||
router.LoadHTMLGlob(config.Templates.HTMLPath + "/*")
|
||||
|
||||
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, router
|
||||
}
|
||||
|
||||
func GetMockedFormContext(formData url.Values, url string) (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
router := gin.New()
|
||||
|
||||
// Load HTML templates
|
||||
router.LoadHTMLGlob(config.Templates.HTMLPath + "/*")
|
||||
|
||||
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, router
|
||||
}
|
||||
|
||||
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"}},
|
||||
Licence: nil,
|
||||
ProfilePicture: "",
|
||||
Password: "password123",
|
||||
Company: "",
|
||||
RoleID: 8,
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTestDB(dbPath string) error {
|
||||
err := os.Remove(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSingleTest(tc TestCase) error {
|
||||
c, w, router := tc.SetupContext()
|
||||
tc.RunHandler(c, router)
|
||||
|
||||
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)
|
||||
}
|
||||
26
go-backend/internal/controllers/licenceController.go
Normal file
26
go-backend/internal/controllers/licenceController.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LicenceController struct {
|
||||
Service services.LicenceService
|
||||
}
|
||||
|
||||
func (lc *LicenceController) GetAllCategories(c *gin.Context) {
|
||||
|
||||
categories, err := lc.Service.GetAllCategories()
|
||||
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error retrieving licence categories", http.StatusInternalServerError, "general", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"licence_categories": categories,
|
||||
})
|
||||
}
|
||||
153
go-backend/internal/controllers/membershipController.go
Normal file
153
go-backend/internal/controllers/membershipController.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type MembershipController struct {
|
||||
Service services.MembershipService
|
||||
UserController interface {
|
||||
ExtractUserFromContext(*gin.Context) (*models.User, error)
|
||||
}
|
||||
}
|
||||
|
||||
type MembershipData struct {
|
||||
// APIKey string `json:"api_key"`
|
||||
Subscription models.SubscriptionModel `json:"subscription"`
|
||||
}
|
||||
|
||||
func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
|
||||
var regData MembershipData
|
||||
|
||||
requestUser, err := mc.UserController.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription registrationHandler", http.StatusBadRequest, "general", "server.validation.invalid_user_data")
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Create) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to register subscription", http.StatusForbidden, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(®Data); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Register Subscription
|
||||
logger.Info.Printf("Registering subscription %v", regData.Subscription.Name)
|
||||
id, err := mc.Service.RegisterSubscription(®Data.Subscription)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Couldn't register Membershipmodel: %v", err)
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
c.JSON(http.StatusConflict, "Duplicate subscription name")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotAcceptable, "Couldn't register Membershipmodel")
|
||||
return
|
||||
}
|
||||
logger.Info.Printf("registering subscription: %+v", regData)
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"status": "success",
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) UpdateHandler(c *gin.Context) {
|
||||
var regData MembershipData
|
||||
|
||||
requestUser, err := mc.UserController.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription UpdateHandler", http.StatusBadRequest, "general", "server.validation.no_auth_tokenw")
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusForbidden, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(®Data); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// update Subscription
|
||||
logger.Info.Printf("Updating subscription %v", regData.Subscription.Name)
|
||||
id, err := mc.Service.UpdateSubscription(®Data.Subscription)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Couldn't update Membershipmodel: %v", err)
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
c.JSON(http.StatusConflict, "Duplicate subscription name")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotAcceptable, "Couldn't update Membershipmodel")
|
||||
return
|
||||
}
|
||||
logger.Info.Printf("updating subscription: %+v", regData)
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"status": "success",
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
|
||||
type deleteData struct {
|
||||
Subscription struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"subscription"`
|
||||
}
|
||||
|
||||
var data deleteData
|
||||
requestUser, err := mc.UserController.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription UpdateHandler", http.StatusBadRequest, "general", "server.validation.no_auth_tokenw")
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Delete) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusForbidden, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := mc.Service.DeleteSubscription(&data.Subscription.ID, &data.Subscription.Name); err != nil {
|
||||
utils.RespondWithError(c, err, "Error during subscription Deletion", http.StatusExpectationFailed, "subscription", "server.error.not_possible")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Subscription deleted successfully"})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) GetSubscriptions(c *gin.Context) {
|
||||
subscriptions, err := mc.Service.GetSubscriptions(nil)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Error retrieving subscriptions: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "validation.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"subscriptions": subscriptions,
|
||||
})
|
||||
}
|
||||
393
go-backend/internal/controllers/membershipController_test.go
Normal file
393
go-backend/internal/controllers/membershipController_test.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/database"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RegisterSubscriptionTest struct {
|
||||
WantDBData map[string]interface{}
|
||||
Input string
|
||||
Name string
|
||||
WantResponse int
|
||||
Assert bool
|
||||
}
|
||||
|
||||
type UpdateSubscriptionTest struct {
|
||||
WantDBData map[string]interface{}
|
||||
Input string
|
||||
Name string
|
||||
WantResponse int
|
||||
Assert bool
|
||||
}
|
||||
|
||||
type DeleteSubscriptionTest struct {
|
||||
WantDBData map[string]interface{}
|
||||
Input string
|
||||
Name string
|
||||
WantResponse int
|
||||
Assert bool
|
||||
}
|
||||
|
||||
type MockUserController struct {
|
||||
UserController // Embed the UserController
|
||||
}
|
||||
|
||||
func (m *MockUserController) ExtractUserFromContext(c *gin.Context) (*models.User, error) {
|
||||
return &models.User{
|
||||
ID: 1,
|
||||
FirstName: "Admin",
|
||||
LastName: "User",
|
||||
Email: "admin@test.com",
|
||||
RoleID: constants.Roles.Admin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setupMockAuth() {
|
||||
// Create and assign the mock controller
|
||||
mockController := &MockUserController{}
|
||||
Mc.UserController = mockController
|
||||
}
|
||||
|
||||
func testMembershipController(t *testing.T) {
|
||||
|
||||
setupMockAuth()
|
||||
tests := getSubscriptionRegistrationData()
|
||||
for _, tt := range tests {
|
||||
logger.Error.Print("==============================================================")
|
||||
logger.Error.Printf("MembershipController : %v", tt.Name)
|
||||
logger.Error.Print("==============================================================")
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if err := runSingleTest(&tt); err != nil {
|
||||
t.Errorf("Test failed: %v", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
updateTests := getSubscriptionUpdateData()
|
||||
for _, tt := range updateTests {
|
||||
logger.Error.Print("==============================================================")
|
||||
logger.Error.Printf("Update SubscriptionData : %v", tt.Name)
|
||||
logger.Error.Print("==============================================================")
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if err := runSingleTest(&tt); err != nil {
|
||||
t.Errorf("Test failed: %v", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
deleteTests := getSubscriptionDeleteData()
|
||||
for _, tt := range deleteTests {
|
||||
logger.Error.Print("==============================================================")
|
||||
logger.Error.Printf("Delete SubscriptionData : %v", tt.Name)
|
||||
logger.Error.Print("==============================================================")
|
||||
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, *gin.Engine) {
|
||||
return GetMockedJSONContext([]byte(rt.Input), "api/subscription")
|
||||
}
|
||||
|
||||
func (rt *RegisterSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
|
||||
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 (ut *UpdateSubscriptionTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
|
||||
return GetMockedJSONContext([]byte(ut.Input), "api/subscription/upsert")
|
||||
}
|
||||
|
||||
func (ut *UpdateSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
|
||||
Mc.UpdateHandler(c)
|
||||
}
|
||||
|
||||
func (ut *UpdateSubscriptionTest) ValidateResponse(w *httptest.ResponseRecorder) error {
|
||||
if w.Code != ut.WantResponse {
|
||||
return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, ut.WantResponse)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ut *UpdateSubscriptionTest) ValidateResult() error {
|
||||
return validateSubscription(ut.Assert, ut.WantDBData)
|
||||
}
|
||||
|
||||
func (dt *DeleteSubscriptionTest) SetupContext() (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
|
||||
return GetMockedJSONContext([]byte(dt.Input), "api/subscription/delete")
|
||||
}
|
||||
|
||||
func (dt *DeleteSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
|
||||
Mc.DeleteSubscription(c)
|
||||
}
|
||||
|
||||
func (dt *DeleteSubscriptionTest) ValidateResponse(w *httptest.ResponseRecorder) error {
|
||||
if w.Code != dt.WantResponse {
|
||||
return fmt.Errorf("Didn't get the expected response code: got: %v; expected: %v", w.Code, dt.WantResponse)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dt *DeleteSubscriptionTest) ValidateResult() error {
|
||||
return validateSubscription(dt.Assert, dt.WantDBData)
|
||||
}
|
||||
|
||||
func getBaseSubscription() MembershipData {
|
||||
return MembershipData{
|
||||
// APIKey: config.Auth.APIKEY,
|
||||
Subscription: models.SubscriptionModel{
|
||||
Name: "Premium",
|
||||
Details: "A subscription detail",
|
||||
MonthlyFee: 12.0,
|
||||
HourlyRate: 14.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
func customizeSubscription(customize func(MembershipData) MembershipData) MembershipData {
|
||||
subscription := getBaseSubscription()
|
||||
return customize(subscription)
|
||||
}
|
||||
|
||||
func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
|
||||
return []RegisterSubscriptionTest{
|
||||
{
|
||||
Name: "Missing details should fail",
|
||||
WantResponse: http.StatusBadRequest,
|
||||
WantDBData: map[string]interface{}{"name": "Just a Subscription"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Details = ""
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Missing model name should fail",
|
||||
WantResponse: http.StatusBadRequest,
|
||||
WantDBData: map[string]interface{}{"name": ""},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = ""
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Negative monthly fee should fail",
|
||||
WantResponse: http.StatusBadRequest,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {
|
||||
sub.Subscription.MonthlyFee = -10.0
|
||||
return sub
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Negative hourly rate should fail",
|
||||
WantResponse: http.StatusBadRequest,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {
|
||||
sub.Subscription.HourlyRate = -1.0
|
||||
return sub
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "correct entry should pass",
|
||||
WantResponse: http.StatusCreated,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Conditions = "Some Condition"
|
||||
subscription.Subscription.IncludedPerYear = 0
|
||||
subscription.Subscription.IncludedPerMonth = 1
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Duplicate subscription name should fail",
|
||||
WantResponse: http.StatusConflict,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: true, // The original subscription should still exist
|
||||
Input: GenerateInputJSON(getBaseSubscription()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getSubscriptionUpdateData() []UpdateSubscriptionTest {
|
||||
return []UpdateSubscriptionTest{
|
||||
{
|
||||
Name: "Modified Monthly Fee, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "Premium", "monthly_fee": "12"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.MonthlyFee = 123.0
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Missing ID, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.ID = 0
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Modified Hourly Rate, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "Premium", "hourly_rate": "14"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.HourlyRate = 3254.0
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "IncludedPerYear changed, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "Premium", "included_per_year": "0"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.IncludedPerYear = 9873.0
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "IncludedPerMonth changed, should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "Premium", "included_per_month": "1"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.IncludedPerMonth = 23415.0
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Update non-existent subscription should fail",
|
||||
WantResponse: http.StatusNotAcceptable,
|
||||
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = "NonExistentSubscription"
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Correct Update should pass",
|
||||
WantResponse: http.StatusAccepted,
|
||||
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Details = "Altered Details"
|
||||
subscription.Subscription.Conditions = "Some Condition"
|
||||
subscription.Subscription.IncludedPerYear = 0
|
||||
subscription.Subscription.IncludedPerMonth = 1
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getSubscriptionDeleteData() []DeleteSubscriptionTest {
|
||||
|
||||
var premiumSub, basicSub models.SubscriptionModel
|
||||
database.DB.Where("name = ?", "Premium").First(&premiumSub)
|
||||
database.DB.Where("name = ?", "Basic").First(&basicSub)
|
||||
|
||||
logger.Error.Printf("premiumSub.ID: %v", premiumSub.ID)
|
||||
logger.Error.Printf("basicSub.ID: %v", basicSub.ID)
|
||||
return []DeleteSubscriptionTest{
|
||||
{
|
||||
Name: "Delete non-existent subscription should fail",
|
||||
WantResponse: http.StatusExpectationFailed,
|
||||
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = "NonExistentSubscription"
|
||||
subscription.Subscription.ID = basicSub.ID
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Delete subscription without name should fail",
|
||||
WantResponse: http.StatusExpectationFailed,
|
||||
WantDBData: map[string]interface{}{"name": ""},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = ""
|
||||
subscription.Subscription.ID = basicSub.ID
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Delete subscription with users should fail",
|
||||
WantResponse: http.StatusExpectationFailed,
|
||||
WantDBData: map[string]interface{}{"name": "Basic"},
|
||||
Assert: true,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = "Basic"
|
||||
subscription.Subscription.ID = basicSub.ID
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
{
|
||||
Name: "Delete valid subscription should succeed",
|
||||
WantResponse: http.StatusOK,
|
||||
WantDBData: map[string]interface{}{"name": "Premium"},
|
||||
Assert: false,
|
||||
Input: GenerateInputJSON(
|
||||
customizeSubscription(func(subscription MembershipData) MembershipData {
|
||||
subscription.Subscription.Name = "Premium"
|
||||
subscription.Subscription.ID = premiumSub.ID
|
||||
return subscription
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
105
go-backend/internal/controllers/user_Password.go
Normal file
105
go-backend/internal/controllers/user_Password.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) {
|
||||
|
||||
// Expected data from the user
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
// find user
|
||||
db_user, err := uc.Service.GetUserByEmail(input.Email)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "couldn't get user by email", http.StatusNotFound, "user.user", "user.email")
|
||||
return
|
||||
}
|
||||
|
||||
// check if user may change the password
|
||||
if db_user.Status <= constants.DisabledStatus {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "User password change request denied, user is disabled", http.StatusForbidden, errors.Responses.Fields.Login, errors.Responses.Keys.UserDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
// create token
|
||||
token, err := uc.Service.HandlePasswordChangeRequest(db_user)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "couldn't handle password change request", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// send email
|
||||
if err := uc.EmailService.SendChangePasswordEmail(db_user, &token); err != nil {
|
||||
utils.RespondWithError(c, err, "Couldn't send change password email", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"message": "password_change_requested",
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) ChangePassword(c *gin.Context) {
|
||||
// Expected data from the user
|
||||
var input struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
Token string `json:"token" binding:"required"`
|
||||
}
|
||||
userIDint, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Invalid user ID", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.InvalidUserID)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
verification, err := uc.Service.VerifyUser(&input.Token, &constants.VerificationTypes.Password)
|
||||
if err != nil || uint(userIDint) != verification.UserID {
|
||||
if err == errors.ErrAlreadyVerified {
|
||||
utils.RespondWithError(c, err, "User already changed password", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.PasswordAlreadyChanged)
|
||||
} else if err.Error() == "record not found" {
|
||||
utils.RespondWithError(c, err, "Couldn't find verification. This is most probably a outdated token.", http.StatusGone, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
} else {
|
||||
utils.RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := uc.Service.GetUserByID(verification.UserID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.UserNotFoundWrongPassword)
|
||||
return
|
||||
}
|
||||
|
||||
user.Status = constants.ActiveStatus
|
||||
user.Verification = *verification
|
||||
user.ID = verification.UserID
|
||||
user.Password = input.Password
|
||||
|
||||
_, err = uc.Service.UpdateUser(user)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "password_changed",
|
||||
})
|
||||
}
|
||||
366
go-backend/internal/controllers/user_controller.go
Normal file
366
go-backend/internal/controllers/user_controller.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/config"
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/middlewares"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/internal/validation"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
Service services.UserServiceInterface
|
||||
EmailService *services.EmailService
|
||||
ConsentService services.ConsentServiceInterface
|
||||
BankAccountService services.BankAccountServiceInterface
|
||||
MembershipService services.MembershipServiceInterface
|
||||
LicenceService services.LicenceInterface
|
||||
}
|
||||
|
||||
type RegistrationData struct {
|
||||
User models.User `json:"user"`
|
||||
}
|
||||
|
||||
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
|
||||
requestUser, err := uc.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in CurrentUserHandler", http.StatusBadRequest, "general", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": requestUser.Safe(),
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) GetAllUsers(c *gin.Context) {
|
||||
|
||||
requestUser, err := uc.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in UpdateHandler", http.StatusBadRequest, "general", "server.validation.no_auth_tokenw")
|
||||
return
|
||||
}
|
||||
if requestUser.RoleID == constants.Roles.Member {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update user", http.StatusForbidden, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
users, err := uc.Service.GetUsers(nil)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error getting users in GetAllUsers", http.StatusInternalServerError, "user.user", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a slice to hold the safe user representations
|
||||
safeUsers := make([]map[string]interface{}, len(*users))
|
||||
|
||||
// Convert each user to its safe representation
|
||||
for i, user := range *users {
|
||||
safeUsers[i] = user.Safe()
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) UpdateHandler(c *gin.Context) {
|
||||
// 1. Extract and validate the user ID from the route
|
||||
requestUser, err := uc.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in UpdateHandler", http.StatusBadRequest, "general", "server.validation.no_auth_tokenw")
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var updateData RegistrationData
|
||||
if err := c.ShouldBindJSON(&updateData); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
user = updateData.User
|
||||
|
||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) && user.ID != requestUser.ID {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update user", http.StatusUnauthorized, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
existingUser, err := uc.Service.GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error finding an existing user", http.StatusNotFound, "user.user", "server.error.not_found")
|
||||
return
|
||||
}
|
||||
// user.Membership.ID = existingUser.Membership.ID
|
||||
|
||||
// user.MembershipID = existingUser.MembershipID
|
||||
// if existingUser.Licence != nil {
|
||||
// user.Licence.ID = existingUser.Licence.ID
|
||||
// }
|
||||
// user.LicenceID = existingUser.LicenceID
|
||||
// user.BankAccount.ID = existingUser.BankAccount.ID
|
||||
// user.BankAccountID = existingUser.BankAccountID
|
||||
|
||||
if requestUser.RoleID <= constants.Priviliges.View {
|
||||
existingUser.Password = ""
|
||||
if err := utils.FilterAllowedStructFields(&user, existingUser, constants.MemberUpdateFields, ""); err != nil {
|
||||
if err.Error() == "Not authorized" {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Trying to update unauthorized fields", http.StatusUnauthorized, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(c, err, "Error filtering users input fields", http.StatusInternalServerError, "user.user", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updatedUser, err := uc.Service.UpdateUser(&user)
|
||||
if err != nil {
|
||||
utils.HandleUserUpdateError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info.Printf("User %d updated successfully by user %d", updatedUser.ID, requestUser.ID)
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "User updated successfully", "user": updatedUser.Safe()})
|
||||
}
|
||||
|
||||
func (uc *UserController) DeleteUser(c *gin.Context) {
|
||||
|
||||
requestUser, err := uc.ExtractUserFromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in DeleteUser", http.StatusBadRequest, "general", "server.validation.no_auth_tokenw")
|
||||
return
|
||||
}
|
||||
|
||||
type deleteData struct {
|
||||
User struct {
|
||||
ID uint `json:"id"`
|
||||
LastName string `json:"last_name"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
var data deleteData
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.HasPrivilige(requestUser, constants.Priviliges.Delete) && data.User.ID != requestUser.ID {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to delete user", http.StatusForbidden, "user.user", "server.error.unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error.Printf("Deleting user: %v", data.User)
|
||||
if err := uc.Service.DeleteUser(data.User.LastName, data.User.ID); err != nil {
|
||||
utils.RespondWithError(c, err, "Error during user deletion", http.StatusInternalServerError, "user.user", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
|
||||
}
|
||||
|
||||
func (uc *UserController) ExtractUserFromContext(c *gin.Context) (*models.User, error) {
|
||||
|
||||
tokenString, err := c.Cookie("jwt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, claims, err := middlewares.ExtractContentFrom(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jwtUserID := uint((*claims)["user_id"].(float64))
|
||||
user, err := uc.Service.GetUserByID(jwtUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (uc *UserController) LogoutHandler(c *gin.Context) {
|
||||
tokenString, err := c.Cookie("jwt")
|
||||
if err != nil {
|
||||
logger.Error.Printf("unable to get token from cookie: %#v", err)
|
||||
}
|
||||
|
||||
middlewares.InvalidateSession(tokenString)
|
||||
|
||||
c.SetCookie("jwt", "", -1, "/", "", true, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
|
||||
}
|
||||
|
||||
func (uc *UserController) LoginHandler(c *gin.Context) {
|
||||
var input struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
utils.RespondWithError(c, err, "Invalid JSON or malformed request", http.StatusBadRequest, errors.Responses.Fields.General, errors.Responses.Keys.Invalid)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := uc.Service.GetUserByEmail(input.Email)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Login Error; user not found", http.StatusNotFound,
|
||||
errors.Responses.Fields.Login,
|
||||
errors.Responses.Keys.UserNotFoundWrongPassword)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status <= constants.DisabledStatus {
|
||||
utils.RespondWithError(c, fmt.Errorf("User banned from login %v %v", user.FirstName, user.LastName),
|
||||
"Login Error; user is disabled",
|
||||
http.StatusNotAcceptable,
|
||||
errors.Responses.Fields.Login,
|
||||
errors.Responses.Keys.UserDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := user.PasswordMatches(input.Password)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Login Error; password comparisson failed", http.StatusInternalServerError, errors.Responses.Fields.Login, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
utils.RespondWithError(c, fmt.Errorf("%v %v(%v)", user.FirstName, user.LastName, user.Email),
|
||||
"Login Error; wrong password",
|
||||
http.StatusNotAcceptable,
|
||||
errors.Responses.Fields.Login,
|
||||
errors.Responses.Keys.UserNotFoundWrongPassword)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error.Printf("jwtsecret: %v", config.Auth.JWTSecret)
|
||||
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error generating token in LoginHandler", http.StatusInternalServerError, errors.Responses.Fields.Login, errors.Responses.Keys.JwtGenerationFailed)
|
||||
return
|
||||
}
|
||||
|
||||
utils.SetCookie(c, token)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Login successful",
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) RegisterUser(c *gin.Context) {
|
||||
|
||||
var regData RegistrationData
|
||||
logger.Error.Printf("registering user...")
|
||||
if err := c.ShouldBindJSON(®Data); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info.Printf("Registering user %v", regData.User.Email)
|
||||
selectedModel, err := uc.MembershipService.GetSubscriptionByName(®Data.User.Membership.SubscriptionModel.Name)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error in Registeruser, couldn't get selected model", http.StatusNotFound, "subscription_model", "server.validation.subscription_model_not_found")
|
||||
return
|
||||
}
|
||||
regData.User.Membership.SubscriptionModel = *selectedModel
|
||||
if selectedModel.RequiredMembershipField != "" {
|
||||
if err := validation.CheckParentMembershipID(regData.User.Membership); err != nil {
|
||||
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't check parent membership id", http.StatusBadRequest, "parent_membership_id", "server.validation.parent_membership_id_not_found")
|
||||
return
|
||||
}
|
||||
}
|
||||
regData.User.RoleID = constants.Roles.Member
|
||||
|
||||
// Register User
|
||||
id, token, err := uc.Service.RegisterUser(®Data.User)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Couldn't register User(%v): %v", regData.User.Email, err)
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
|
||||
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't register user", http.StatusConflict, "email", "server.validation.email_already_exists")
|
||||
} else {
|
||||
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't register user", http.StatusConflict, "general", "server.error.internal_server_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
regData.User.ID = id
|
||||
|
||||
// Register Consents
|
||||
var consents = [2]models.Consent{
|
||||
{
|
||||
FirstName: regData.User.FirstName,
|
||||
LastName: regData.User.LastName,
|
||||
Email: regData.User.Email,
|
||||
ConsentType: "TermsOfService",
|
||||
},
|
||||
{
|
||||
FirstName: regData.User.FirstName,
|
||||
LastName: regData.User.LastName,
|
||||
Email: regData.User.Email,
|
||||
ConsentType: "Privacy",
|
||||
},
|
||||
}
|
||||
for _, consent := range consents {
|
||||
_, err = uc.ConsentService.RegisterConsent(&consent)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't register consent", http.StatusInternalServerError, "general", "server.error.internal_server_error")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send notifications
|
||||
if err := uc.EmailService.SendVerificationEmail(®Data.User, &token); err != nil {
|
||||
logger.Error.Printf("Failed to send email verification email to user(%v): %v", regData.User.Email, err)
|
||||
// Proceed without returning error since user registration is successful
|
||||
// TODO Notify Admin
|
||||
}
|
||||
|
||||
// Notify admin of new user registration
|
||||
if err := uc.EmailService.SendRegistrationNotification(®Data.User); err != nil {
|
||||
logger.Error.Printf("Failed to notify admin of new user(%v) registration: %v", regData.User.Email, err)
|
||||
// Proceed without returning error since user registration is successful
|
||||
// TODO Notify Admin
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Registration successuful",
|
||||
"id": regData.User.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) VerifyMailHandler(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
logger.Error.Println("Missing token to verify mail")
|
||||
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Missing token"})
|
||||
return
|
||||
}
|
||||
|
||||
verification, err := uc.Service.VerifyUser(&token, &constants.VerificationTypes.Email)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Cannot verify user: %v", err)
|
||||
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
|
||||
}
|
||||
|
||||
user, err := uc.Service.GetUserByID(verification.UserID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.UserNotFoundWrongPassword)
|
||||
return
|
||||
}
|
||||
|
||||
user.Status = constants.VerifiedStatus
|
||||
user.Verification = *verification
|
||||
user.ID = verification.UserID
|
||||
user.Password = ""
|
||||
|
||||
uc.Service.UpdateUser(user)
|
||||
logger.Info.Printf("Verified User: %#v", user.Email)
|
||||
|
||||
uc.EmailService.SendWelcomeEmail(user)
|
||||
c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName})
|
||||
}
|
||||
1228
go-backend/internal/controllers/user_controller_test.go
Normal file
1228
go-backend/internal/controllers/user_controller_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user