diff --git a/internal/models/newsletter.go b/internal/models/newsletter.go index 9d7f943..a0aa49a 100644 --- a/internal/models/newsletter.go +++ b/internal/models/newsletter.go @@ -55,12 +55,19 @@ func (n *NewsLetter) GetDeletedNewsLetterById(db database.DatabaseManager, ID st } func (n *NewsLetter) CreateNewsLetter(db database.DatabaseManager) error { - err := db.CreateOneRecord(&n) + unique, uniqueErr := utility.CheckForUniqueness(db, &NewsLetter{}, "email", n.Email) + if uniqueErr != nil { + return uniqueErr + } + if !unique { + return fmt.Errorf("email already subscribed") + } + + err := db.CreateOneRecord(&n) if err != nil { return err } - return nil } @@ -76,6 +83,15 @@ func (n *NewsLetter) DeleteNewsLetter(db database.DatabaseManager) error { } func (n *NewsLetter) UpdateNewsLetter(db database.DatabaseManager) error { + unique, uniqueErr := utility.CheckForUniqueness(db, &NewsLetter{}, "email", n.Email) + + if uniqueErr != nil { + return uniqueErr + } + if !unique { + return fmt.Errorf("email already subscribed") + } + _, err := db.SaveAllFields(&n) return err } diff --git a/internal/models/organisation.go b/internal/models/organisation.go index 7134c63..7051d72 100644 --- a/internal/models/organisation.go +++ b/internal/models/organisation.go @@ -2,6 +2,7 @@ package models import ( "errors" + "fmt" "math" "time" @@ -10,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/database" "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/postgresql" + "github.com/hngprojects/hng_boilerplate_golang_web/utility" ) type Organisation struct { @@ -64,6 +66,15 @@ type AddUserToOrgRequestModel struct { } func (c *Organisation) CreateOrganisation(db database.DatabaseManager) error { + unique, uniqueErr := utility.CheckForUniqueness(db, &Organisation{}, "email", c.Email) + + if uniqueErr != nil { + return uniqueErr + } + if !unique { + return fmt.Errorf("the email %s already exists in this organisation", c.Email) + } + err := db.CreateOneRecord(&c) if err != nil { return err diff --git a/internal/models/role.go b/internal/models/role.go index 1c2095a..fac4a2b 100644 --- a/internal/models/role.go +++ b/internal/models/role.go @@ -10,6 +10,7 @@ import ( "gorm.io/gorm" "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/database" + "github.com/hngprojects/hng_boilerplate_golang_web/utility" ) type RoleName string @@ -74,13 +75,21 @@ func (p PermissionList) Value() (driver.Value, error) { } func (r *OrgRole) CreateOrgRole(db database.DatabaseManager) error { - err := db.CreateOneRecord(&r) + isUniqueName, errName := utility.CheckForUniqueness(db, &OrgRole{}, "name", r.Name) + isUniqueId, errId := utility.CheckForUniqueness(db, &OrgRole{}, "organisation_id", r.OrganisationID) - if err != nil { - return err + if errName != nil { + return errName + } + if errId != nil { + return errId + } + if !isUniqueName || !isUniqueId { + return fmt.Errorf("Role with the name %s already exists in this organisation", r.Name) } - return nil + createError := db.CreateOneRecord(&r) + return createError } func (r *OrgRole) DeleteOrgRole(db database.DatabaseManager) error { diff --git a/internal/models/seed/seed.go b/internal/models/seed/seed.go index f898e38..62b4cb1 100644 --- a/internal/models/seed/seed.go +++ b/internal/models/seed/seed.go @@ -79,9 +79,9 @@ func SeedDatabase(db database.DatabaseManager) { // Create organisations and categories organisations := []models.Organisation{ - {ID: utility.GenerateUUID(), Name: "Org1", Email: fmt.Sprintf(utility.RandomString(4) + "@email.com"), Description: "Description1", OwnerID: Userid1}, - {ID: utility.GenerateUUID(), Name: "Org2", Email: fmt.Sprintf(utility.RandomString(4) + "@email.com"), Description: "Description2", OwnerID: Userid1}, - {ID: utility.GenerateUUID(), Name: "Org3", Email: fmt.Sprintf(utility.RandomString(4) + "@email.com"), Description: "Description3", OwnerID: Userid2}, + {ID: utility.GenerateUUID(), Name: "Org1", Email: fmt.Sprintln(utility.RandomString(4) + "@email.com"), Description: "Description1", OwnerID: Userid1}, + {ID: utility.GenerateUUID(), Name: "Org2", Email: fmt.Sprintln(utility.RandomString(4) + "@email.com"), Description: "Description2", OwnerID: Userid1}, + {ID: utility.GenerateUUID(), Name: "Org3", Email: fmt.Sprintln(utility.RandomString(4) + "@email.com"), Description: "Description3", OwnerID: Userid2}, } var existingUser models.User @@ -166,7 +166,6 @@ func SeedDatabase(db database.DatabaseManager) { } func SeedTestDatabase(db database.DatabaseManager) { - roles := []models.Role{ {ID: int(models.RoleIdentity.User), Name: "user", Description: "user related functions"}, {ID: int(models.RoleIdentity.SuperAdmin), Name: "super admin", Description: "super admin related functions"}, @@ -175,20 +174,16 @@ func SeedTestDatabase(db database.DatabaseManager) { var existingRole models.Role if err := db.DB().Where("id = ?", roles[0].ID).First(&existingRole).Error; err != nil { if err == gorm.ErrRecordNotFound { - db.CreateMultipleRecords(&roles, len(roles)) } else { fmt.Println("An error occurred: ", err) } - } else { fmt.Println("Roles already exist, skipping seeding.") } - } func SeedOrgRolesAndPermissions(db database.DatabaseManager) { - var organizations []models.Organisation if err := db.DB().Find(&organizations).Error; err != nil { fmt.Printf("Error fetching organizations: %v\n", err) @@ -202,10 +197,33 @@ func SeedOrgRolesAndPermissions(db database.DatabaseManager) { } for _, role := range roles { - if err := db.CreateOneRecord(&role); err != nil { - fmt.Printf("Error creating role: %v\n", err) + isUniqueName, errName := utility.CheckForUniqueness(db, &models.OrgRole{}, "name", role.Name) + isUniqueId, errId := utility.CheckForUniqueness(db, &models.OrgRole{}, "organisation_id", role.OrganisationID) + + if errName != nil { + fmt.Printf("Error checking role name uniqueness: %v", errName) continue } + if errId != nil { + fmt.Printf("Error checking role id uniqueness: %v", errId) + continue + } + if isUniqueName && isUniqueId { + if err := db.CreateOneRecord(&role); err != nil { + fmt.Printf("Error creating role: %v\n", err) + continue + } + } else { + fmt.Printf("Role %s already exists in organisation %s\n", role.Name, org.Name) + var existingRole models.OrgRole + result := db.DB().Where("name = ? AND organisation_id = ?", role.Name, role.OrganisationID).First(&existingRole) + if result.Error != nil { + fmt.Printf("Error fetching existing role: %v\n", result.Error) + continue + } + role.ID = existingRole.ID + fmt.Printf("Using existing role: ID %s", role.ID) + } permissions := []models.Permission{ {ID: utility.GenerateUUID(), RoleID: role.ID, Category: "Transactions", PermissionList: map[string]bool{"can_view_transactions": true, "can_edit_transactions": true}}, @@ -213,8 +231,30 @@ func SeedOrgRolesAndPermissions(db database.DatabaseManager) { } for _, permission := range permissions { - if err := db.CreateOneRecord(&permission); err != nil { - fmt.Printf("Error creating permission: %v\n", err) + isUniqueId, errId := utility.CheckForUniqueness(db, &models.Permission{}, "role_id", permission.RoleID) + isUniqueCategory, errCategory := utility.CheckForUniqueness(db, &models.Permission{}, "category", permission.Category) + if errId != nil { + fmt.Printf("Error checking permissions id uniqueness: %v", errId) + continue + } + if errCategory != nil { + fmt.Printf("Error checking permissions category uniqueness: %v", errCategory) + continue + } + if isUniqueId && isUniqueCategory { + if err := db.CreateOneRecord(&permission); err != nil { + fmt.Printf("Error creating permission: %v\n", err) + } + } else { + fmt.Printf("Category %s already exists for role ID %s in permissions\n", permission.Category, permission.RoleID) + var existingPermission models.Permission + result := db.DB().Where("role_id = ? AND category = ?", permission.RoleID, permission.Category).First(&existingPermission) + if result.Error != nil { + fmt.Printf("Error fetching existing permission: %v\n", result.Error) + continue + } + permission.ID = existingPermission.ID + fmt.Printf("Using existing permission: ID %s", permission.ID) } } } diff --git a/internal/models/superadmin.go b/internal/models/superadmin.go index f26a154..266dfe0 100644 --- a/internal/models/superadmin.go +++ b/internal/models/superadmin.go @@ -2,6 +2,7 @@ package models import ( "errors" + "fmt" "time" "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/database" @@ -95,6 +96,18 @@ func (u *UserRegionTimezoneLanguage) CreateUserRegion(db database.DatabaseManage } func (l *Language) CreateLanguage(db database.DatabaseManager) error { + isUniqueName, errName := utility.CheckForUniqueness(db, &Language{}, "name", l.Name) + isUniqueCode, errCode := utility.CheckForUniqueness(db, &Language{}, "code", l.Code) + + if errName != nil { + return errName + } + if errCode != nil { + return errCode + } + if !isUniqueName || !isUniqueCode { + return fmt.Errorf("name already exists") + } err := db.CreateOneRecord(&l) if err != nil { @@ -105,8 +118,20 @@ func (l *Language) CreateLanguage(db database.DatabaseManager) error { } func (t *Timezone) CreateTimeZone(db database.DatabaseManager) error { - err := db.CreateOneRecord(&t) + uniqueTime, uniqueErrTime := utility.CheckForUniqueness(db, &Timezone{}, "timezone", t.Timezone) + uniqueGmt, uniqueErrGmt := utility.CheckForUniqueness(db, &Timezone{}, "gmt_offset", t.GmtOffset) + if uniqueErrTime != nil { + return uniqueErrTime + } + if uniqueErrGmt != nil { + return uniqueErrGmt + } + if !uniqueTime || !uniqueGmt { + return fmt.Errorf("timezone already exists") + } + + err := db.CreateOneRecord(&t) if err != nil { return err } @@ -115,8 +140,20 @@ func (t *Timezone) CreateTimeZone(db database.DatabaseManager) error { } func (r *Region) CreateRegion(db database.DatabaseManager) error { - err := db.CreateOneRecord(&r) + isUniqueName, errName := utility.CheckForUniqueness(db, &Region{}, "name", r.Name) + isUniqueCode, errCode := utility.CheckForUniqueness(db, &Region{}, "code", r.Code) + + if errName != nil { + return errName + } + if errCode != nil { + return errCode + } + if !isUniqueName || !isUniqueCode { + return fmt.Errorf("name already exists") + } + err := db.CreateOneRecord(&r) if err != nil { return err } @@ -181,6 +218,18 @@ func (t *Timezone) GetTimezoneByID(db database.DatabaseManager, ID string) (Time } func (t *Timezone) UpdateTimeZone(db database.DatabaseManager) error { + uniqueTime, uniqueErrTime := utility.CheckForUniqueness(db, &Timezone{}, "timezone", t.Timezone) + uniqueGmt, uniqueErrGmt := utility.CheckForUniqueness(db, &Timezone{}, "gmt_offset", t.GmtOffset) + + if uniqueErrTime != nil { + return uniqueErrTime + } + if uniqueErrGmt != nil { + return uniqueErrGmt + } + if !uniqueTime || !uniqueGmt { + return fmt.Errorf("timezone already exists") + } _, err := db.SaveAllFields(&t) return err } diff --git a/pkg/controller/newsletter/newsletter.go b/pkg/controller/newsletter/newsletter.go index 84d0359..e4dba49 100644 --- a/pkg/controller/newsletter/newsletter.go +++ b/pkg/controller/newsletter/newsletter.go @@ -75,7 +75,7 @@ func (base *Controller) SubscribeNewsLetter(c *gin.Context) { err = service.NewsLetterSubscribe(&req, base.Db.Postgresql.DB()) if err != nil { if err == models.ErrEmailAlreadySubscribed { - rd := utility.BuildErrorResponse(http.StatusConflict, "error", "Email already subscribed", nil, nil) + rd := utility.BuildErrorResponse(http.StatusConflict, "error", "email already subscribed", nil, nil) c.JSON(http.StatusConflict, rd) } else { rd := utility.BuildErrorResponse(http.StatusBadRequest, "error", "Failed to subscribe", err, nil) diff --git a/tests/test_newsletter/create_test.go b/tests/test_newsletter/create_test.go index 3cc543e..cfe0b6d 100644 --- a/tests/test_newsletter/create_test.go +++ b/tests/test_newsletter/create_test.go @@ -82,7 +82,7 @@ func TestPostNewsletter_CheckDuplicateEmail(t *testing.T) { response := tst.ParseResponse(resp) tst.AssertStatusCode(t, resp.Code, http.StatusConflict) - tst.AssertResponseMessage(t, response["message"].(string), "Email already subscribed") + tst.AssertResponseMessage(t, response["message"].(string), "email already subscribed") } func TestPostNewsletter_SaveData(t *testing.T) { diff --git a/tests/test_organisation/create_roles_test.go b/tests/test_organisation/create_roles_test.go index 624f99d..56722f8 100644 --- a/tests/test_organisation/create_roles_test.go +++ b/tests/test_organisation/create_roles_test.go @@ -163,4 +163,31 @@ func TestCreateOrgRole(t *testing.T) { response := tests.ParseResponse(resp) tests.AssertResponseMessage(t, response["message"].(string), "Validation failed") }) + + t.Run("Duplicate Create Org Role", func(t *testing.T) { + router, orgController := setup() + + loginData := models.LoginRequestModel{ + Email: adminUser.Email, + Password: "password", + } + token := tests.GetLoginToken(t, router, *orgController, loginData) + + role := models.OrgRole{ + Name: fmt.Sprintf("Admin Role-%v", utility.RandomString(5)), + Description: "New role description", + } + roleJSON, _ := json.Marshal(role) + + duplicateReq, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/organisations/%s/roles", orgID), bytes.NewBuffer(roleJSON)) + duplicateReq.Header.Set("Content-Type", "application/json") + duplicateReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + duplicateResp := httptest.NewRecorder() + router.ServeHTTP(duplicateResp, duplicateReq) + + tests.AssertStatusCode(t, duplicateResp.Code, http.StatusBadRequest) + duplicateResponse := tests.ParseResponse(duplicateResp) + tests.AssertResponseMessage(t, duplicateResponse["message"].(string), fmt.Sprintf("Role with the name %s already exists in this organisation", role.Name)) + }) } diff --git a/tests/test_organisation/update_roles_test.go b/tests/test_organisation/update_roles_test.go index 040b260..5b36a29 100644 --- a/tests/test_organisation/update_roles_test.go +++ b/tests/test_organisation/update_roles_test.go @@ -172,6 +172,7 @@ func TestUpdateOrgRole(t *testing.T) { response := tests.ParseResponse(resp) tests.AssertResponseMessage(t, response["message"].(string), "Validation failed") }) + } func TestUpdateOrgPermissions(t *testing.T) { diff --git a/tests/test_superadmin/language_test.go b/tests/test_superadmin/language_test.go index f5dbd3b..68d983b 100644 --- a/tests/test_superadmin/language_test.go +++ b/tests/test_superadmin/language_test.go @@ -127,6 +127,8 @@ func TestAddToLanguage(t *testing.T) { router.ServeHTTP(resp, req) tests.AssertStatusCode(t, resp.Code, http.StatusBadRequest) + response := tests.ParseResponse(resp) + tests.AssertResponseMessage(t, response["message"].(string), "name already exists") }) t.Run("Unauthorized Access", func(t *testing.T) { diff --git a/tests/test_superadmin/region_test.go b/tests/test_superadmin/region_test.go index ea341c4..f17928e 100644 --- a/tests/test_superadmin/region_test.go +++ b/tests/test_superadmin/region_test.go @@ -212,7 +212,7 @@ func TestGetRegions(t *testing.T) { tests.AssertResponseMessage(t, response["message"].(string), "Regions retrieved successfully") }) - t.Run("Successful Get Regions for user", func(t *testing.T) { + t.Run("Duplicate Get Regions for user", func(t *testing.T) { router, authController := setup() loginData := models.LoginRequestModel{ Email: regularUser.Email, diff --git a/tests/test_superadmin/timezone_test.go b/tests/test_superadmin/timezone_test.go index f806661..3c54f71 100644 --- a/tests/test_superadmin/timezone_test.go +++ b/tests/test_superadmin/timezone_test.go @@ -101,7 +101,7 @@ func TestAddToTimezone(t *testing.T) { response := tests.ParseResponse(resp) tests.AssertResponseMessage(t, response["message"].(string), "Validation failed") }) - + t.Run("Duplicate Timezone", func(t *testing.T) { router, authController := setup() @@ -129,11 +129,13 @@ func TestAddToTimezone(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, "/api/v1/timezones", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - + resp := httptest.NewRecorder() router.ServeHTTP(resp, req) - + tests.AssertStatusCode(t, resp.Code, http.StatusBadRequest) + response := tests.ParseResponse(resp) + tests.AssertResponseMessage(t, response["message"].(string), "timezone already exists") }) t.Run("Unauthorized Access", func(t *testing.T) { diff --git a/utility/check_uniqueness.go b/utility/check_uniqueness.go new file mode 100644 index 0000000..a144a7a --- /dev/null +++ b/utility/check_uniqueness.go @@ -0,0 +1,52 @@ +package utility + +import ( + "fmt" + "strings" + + "github.com/hngprojects/hng_boilerplate_golang_web/pkg/repository/storage/database" + "gorm.io/gorm" +) + +func CheckForUniqueness[T any](dbManager database.DatabaseManager, model T, field string, value interface{}) (bool, error) { + var count int64 + + err := dbManager.DB().Model(model).Where(fmt.Sprintf("%s = ?", field), value).Count(&count).Error + if err != nil { + return false, err + } + return count == 0, nil +} + +func IsUniqueSingleField[T any](db *gorm.DB, model T, field string, value interface{}) (bool, error) { + var count int64 + + err := db.Model(model).Where(fmt.Sprintf("%s = ?", field), value).Count(&count).Error + if err != nil { + return false, err + } + return count == 0, nil +} + +func IsUniqueMultipleFields(db *gorm.DB, model interface{}, fields map[string]interface{}) (bool, error) { + var count int64 + + queryParts := []string{} + values := []interface{}{} + + // Iterate over the fields map to build query dynamically + for field, value := range fields { + queryParts = append(queryParts, fmt.Sprintf("%s = ?", field)) + values = append(values, value) + } + + // Join all conditions using "AND" + query := strings.Join(queryParts, " AND ") + + err := db.Model(model).Where(query, values...).Count(&count).Error + fmt.Printf("Checking uniqueness on fields %+v with count %d\n", fields, count) + if err != nil { + return false, err + } + return count == 0, nil +}