Merge pull request 'Implement phone number validation for customer and user registration and updates' (#35) from Phone-Uniqueness into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/35
This commit is contained in:
@@ -1,7 +1,42 @@
|
||||
package customer
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
var (
|
||||
ErrCustomerNotFound = errors.New("customer not found")
|
||||
orm "tm/pkg/mongo"
|
||||
)
|
||||
|
||||
var ErrCustomerNotFound = errors.New("customer not found")
|
||||
|
||||
func mapCustomerWriteError(err error) error {
|
||||
if err == nil || !orm.IsDuplicateKeyError(err) {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case orm.DuplicateKeyMatchesField(err, "phone"):
|
||||
return errors.New("customer with this phone number already exists")
|
||||
case orm.DuplicateKeyMatchesField(err, "email"):
|
||||
return errors.New("customer with this email already exists")
|
||||
case orm.DuplicateKeyMatchesField(err, "username"):
|
||||
return errors.New("customer with this username already exists")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func isCustomerConflictError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch err.Error() {
|
||||
case "customer with this phone number already exists",
|
||||
"customer with this email already exists",
|
||||
"customer with this username already exists",
|
||||
"company with this name already exists",
|
||||
"company with this registration number already exists",
|
||||
"company with this tax ID already exists":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
|
||||
|
||||
customer, err := h.service.Register(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
if err.Error() == "customer with this email already exists" ||
|
||||
err.Error() == "customer with this username already exists" ||
|
||||
err.Error() == "company with this name already exists" ||
|
||||
err.Error() == "company with this registration number already exists" ||
|
||||
err.Error() == "company with this tax ID already exists" {
|
||||
if isCustomerConflictError(err) {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "invalid company ID") || err.Error() == "invalid username format" {
|
||||
@@ -126,11 +122,7 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
}
|
||||
if err.Error() == "customer with this email already exists" ||
|
||||
err.Error() == "customer with this username already exists" ||
|
||||
err.Error() == "company with this name already exists" ||
|
||||
err.Error() == "company with this registration number already exists" ||
|
||||
err.Error() == "company with this tax ID already exists" {
|
||||
if isCustomerConflictError(err) {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "invalid company ID") {
|
||||
|
||||
@@ -25,6 +25,7 @@ type Repository interface {
|
||||
GetByID(ctx context.Context, id string) (*Customer, error)
|
||||
GetByEmail(ctx context.Context, email string) (*Customer, error)
|
||||
GetByUsername(ctx context.Context, username string) (*Customer, error)
|
||||
GetByPhone(ctx context.Context, phone string) (*Customer, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]Customer, error)
|
||||
GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error)
|
||||
GetByCompanies(ctx context.Context, companies []string) ([]Customer, error)
|
||||
@@ -50,6 +51,8 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
|
||||
indexes := []orm.Index{
|
||||
*orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
|
||||
*orm.CreateTextIndex("search_idx", "email", "username"),
|
||||
*orm.CreateUniqueIndex("phone_idx", bson.D{{Key: "phone", Value: 1}}).
|
||||
WithPartialFilterExpression(bson.M{"phone": bson.M{"$type": "string", "$ne": ""}}),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
@@ -166,6 +169,24 @@ func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Cus
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByPhone retrieves a customer by phone number
|
||||
func (r *customerRepository) GetByPhone(ctx context.Context, phone string) (*Customer, error) {
|
||||
filter := bson.M{"phone": phone}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||
return nil, ErrCustomerNotFound
|
||||
}
|
||||
r.logger.Error("Failed to get customer by phone", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"phone": phone,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByUsername retrieves a customer by username
|
||||
func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) {
|
||||
filter := bson.M{"username": username}
|
||||
|
||||
@@ -95,6 +95,13 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
||||
return nil, errors.New("customer with this username already exists")
|
||||
}
|
||||
|
||||
if form.Phone != nil && *form.Phone != "" {
|
||||
existingCustomer, _ = s.repository.GetByPhone(ctx, *form.Phone)
|
||||
if existingCustomer != nil {
|
||||
return nil, errors.New("customer with this phone number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check validator
|
||||
if !s.validator.IsValidUsername(form.Username) {
|
||||
return nil, errors.New("invalid username format")
|
||||
@@ -149,6 +156,9 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
||||
// Save to database
|
||||
err = s.repository.Register(ctx, customer)
|
||||
if err != nil {
|
||||
if mapped := mapCustomerWriteError(err); mapped != err {
|
||||
return nil, mapped
|
||||
}
|
||||
s.logger.Error("Failed to create customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": form.Email,
|
||||
@@ -373,6 +383,12 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
}
|
||||
|
||||
if form.Phone != nil && (customer.Phone == nil || *form.Phone != *customer.Phone) {
|
||||
if *form.Phone != "" {
|
||||
existingCustomer, _ := s.repository.GetByPhone(ctx, *form.Phone)
|
||||
if existingCustomer != nil && existingCustomer.ID.Hex() != id {
|
||||
return nil, errors.New("customer with this phone number already exists")
|
||||
}
|
||||
}
|
||||
customer.Phone = form.Phone
|
||||
infoChanged = true
|
||||
}
|
||||
@@ -411,6 +427,9 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, customer)
|
||||
if err != nil {
|
||||
if mapped := mapCustomerWriteError(err); mapped != err {
|
||||
return nil, mapped
|
||||
}
|
||||
s.logger.Error("Failed to update customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id,
|
||||
|
||||
+33
-3
@@ -1,7 +1,37 @@
|
||||
package user
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
orm "tm/pkg/mongo"
|
||||
)
|
||||
|
||||
var ErrUserNotFound = errors.New("user not found")
|
||||
|
||||
func mapUserWriteError(err error) error {
|
||||
if err == nil || !orm.IsDuplicateKeyError(err) {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case orm.DuplicateKeyMatchesField(err, "phone"):
|
||||
return errors.New("phone number already exists")
|
||||
case orm.DuplicateKeyMatchesField(err, "email"):
|
||||
return errors.New("email already exists")
|
||||
case orm.DuplicateKeyMatchesField(err, "username"):
|
||||
return errors.New("username already exists")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func isUserConflictError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch err.Error() {
|
||||
case "phone number already exists", "email already exists", "username already exists":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,9 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
|
||||
|
||||
user, err := h.service.Update(c.Request().Context(), userID, form)
|
||||
if err != nil {
|
||||
if isUserConflictError(err) {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
@@ -274,6 +277,9 @@ func (h *Handler) CreateUser(c echo.Context) error {
|
||||
// Call service
|
||||
user, err := h.service.Register(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
if isUserConflictError(err) {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
@@ -386,6 +392,9 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
||||
// Call service
|
||||
user, err := h.service.Update(c.Request().Context(), idStr, form)
|
||||
if err != nil {
|
||||
if isUserConflictError(err) {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ type Repository interface {
|
||||
UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*User, error)
|
||||
GetByPhone(ctx context.Context, phone string) (*User, error)
|
||||
GetByIDs(ctx context.Context, userIDs []string) ([]User, error)
|
||||
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*orm.PaginatedResult[User], error)
|
||||
}
|
||||
@@ -38,6 +39,8 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []orm.Index{
|
||||
*orm.CreateTextIndex("search_idx", "full_name", "email", "username"),
|
||||
*orm.CreateUniqueIndex("phone_idx", bson.D{{Key: "phone", Value: 1}}).
|
||||
WithPartialFilterExpression(bson.M{"phone": bson.M{"$type": "string", "$ne": ""}}),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
@@ -250,6 +253,23 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, e
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByPhone retrieves a user by phone number
|
||||
func (r *userRepository) GetByPhone(ctx context.Context, phone string) (*User, error) {
|
||||
user, err := r.ormRepo.FindOne(ctx, bson.M{"phone": phone})
|
||||
if err != nil {
|
||||
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by phone", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"phone": phone,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByUsername retrieves a user by username
|
||||
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) {
|
||||
user, err := r.ormRepo.FindOne(ctx, bson.M{"username": username})
|
||||
|
||||
@@ -77,6 +77,13 @@ func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
if form.Phone != nil && *form.Phone != "" {
|
||||
existingUser, _ = s.repository.GetByPhone(ctx, *form.Phone)
|
||||
if existingUser != nil {
|
||||
return nil, errors.New("phone number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -110,6 +117,9 @@ func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User
|
||||
// Save to database
|
||||
err = s.repository.Create(ctx, user)
|
||||
if err != nil {
|
||||
if mapped := mapUserWriteError(err); mapped != err {
|
||||
return nil, mapped
|
||||
}
|
||||
s.logger.Error("Failed to register user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": form.Email,
|
||||
@@ -184,7 +194,13 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor
|
||||
user.Position = form.Position
|
||||
}
|
||||
|
||||
if form.Phone != nil {
|
||||
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
|
||||
if *form.Phone != "" {
|
||||
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
|
||||
if existingUser != nil && existingUser.ID.Hex() != id {
|
||||
return nil, errors.New("phone number already exists")
|
||||
}
|
||||
}
|
||||
user.Phone = form.Phone
|
||||
}
|
||||
|
||||
@@ -195,6 +211,9 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, user)
|
||||
if err != nil {
|
||||
if mapped := mapUserWriteError(err); mapped != err {
|
||||
return nil, mapped
|
||||
}
|
||||
s.logger.Error("Failed to update user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id,
|
||||
@@ -573,7 +592,13 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up
|
||||
if form.Position != nil {
|
||||
user.Position = form.Position
|
||||
}
|
||||
if form.Phone != nil {
|
||||
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
|
||||
if *form.Phone != "" {
|
||||
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
|
||||
if existingUser != nil && existingUser.ID.Hex() != userID {
|
||||
return errors.New("phone number already exists")
|
||||
}
|
||||
}
|
||||
user.Phone = form.Phone
|
||||
}
|
||||
if form.ProfileImage != nil {
|
||||
@@ -586,6 +611,9 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up
|
||||
// Save to database
|
||||
err = s.repository.Update(ctx, user)
|
||||
if err != nil {
|
||||
if mapped := mapUserWriteError(err); mapped != err {
|
||||
return mapped
|
||||
}
|
||||
s.logger.Error("Failed to update user profile", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
var dupKeyFieldNamePattern = regexp.MustCompile(`(?:^|[,{]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:`)
|
||||
|
||||
// IsDuplicateKeyError reports whether err is a MongoDB E11000 duplicate key error.
|
||||
func IsDuplicateKeyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(duplicateKeyMessages(err)) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if hasStructuredWriteException(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "e11000") || strings.Contains(msg, "duplicate key")
|
||||
}
|
||||
|
||||
// DuplicateKeyMatchesField reports whether a duplicate key error involves the given field
|
||||
// by parsing WriteException messages (index name and dup key document keys), not broad
|
||||
// substring matching on the full error text.
|
||||
func DuplicateKeyMatchesField(err error, field string) bool {
|
||||
if err == nil || field == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
field = strings.ToLower(field)
|
||||
msgs := duplicateKeyMessages(err)
|
||||
if len(msgs) == 0 {
|
||||
if hasStructuredWriteException(err) {
|
||||
return false
|
||||
}
|
||||
if !IsDuplicateKeyError(err) {
|
||||
return false
|
||||
}
|
||||
msgs = []string{err.Error()}
|
||||
}
|
||||
for _, msg := range msgs {
|
||||
if duplicateKeyMessageMatchesField(msg, field) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func duplicateKeyMessages(err error) []string {
|
||||
var msgs []string
|
||||
|
||||
var writeException mongodriver.WriteException
|
||||
if errors.As(err, &writeException) {
|
||||
for _, writeErr := range writeException.WriteErrors {
|
||||
if writeErr.Code == 11000 {
|
||||
msgs = append(msgs, writeErr.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var bulkWriteException mongodriver.BulkWriteException
|
||||
if errors.As(err, &bulkWriteException) {
|
||||
for _, writeErr := range bulkWriteException.WriteErrors {
|
||||
if writeErr.Code == 11000 {
|
||||
msgs = append(msgs, writeErr.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
func hasStructuredWriteException(err error) bool {
|
||||
var writeException mongodriver.WriteException
|
||||
if errors.As(err, &writeException) {
|
||||
return true
|
||||
}
|
||||
|
||||
var bulkWriteException mongodriver.BulkWriteException
|
||||
return errors.As(err, &bulkWriteException)
|
||||
}
|
||||
|
||||
func duplicateKeyMessageMatchesField(message, field string) bool {
|
||||
message = strings.ToLower(message)
|
||||
|
||||
if indexName := extractDuplicateIndexName(message); indexName != "" {
|
||||
if indexNameMatchesField(indexName, field) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range extractDuplicateKeyFields(message) {
|
||||
if key == field {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func extractDuplicateIndexName(message string) string {
|
||||
const prefix = "index: "
|
||||
start := strings.Index(message, prefix)
|
||||
if start < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
rest := strings.TrimSpace(message[start+len(prefix):])
|
||||
if rest == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
end := strings.IndexByte(rest, ' ')
|
||||
if end >= 0 {
|
||||
return rest[:end]
|
||||
}
|
||||
|
||||
return rest
|
||||
}
|
||||
|
||||
func indexNameMatchesField(indexName, field string) bool {
|
||||
switch indexName {
|
||||
case field, field + "_idx", field + "_1":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractDuplicateKeyFields(message string) []string {
|
||||
const prefix = "dup key:"
|
||||
start := strings.Index(message, prefix)
|
||||
if start < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
document := strings.TrimSpace(message[start+len(prefix):])
|
||||
if !strings.HasPrefix(document, "{") {
|
||||
return nil
|
||||
}
|
||||
|
||||
depth := 0
|
||||
end := -1
|
||||
for i, char := range document {
|
||||
switch char {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
end = i
|
||||
}
|
||||
}
|
||||
if end >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if end <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
inner := strings.TrimSpace(document[1:end])
|
||||
matches := dupKeyFieldNamePattern.FindAllStringSubmatch(inner, -1)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := make([]string, 0, len(matches))
|
||||
seen := make(map[string]struct{}, len(matches))
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(match[1])
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
fields = append(fields, key)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
func TestIsDuplicateKeyError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated error",
|
||||
err: errors.New("connection refused"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "write exception duplicate key",
|
||||
err: writeExceptionWithMessage(
|
||||
11000,
|
||||
`E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`,
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "write exception non duplicate code",
|
||||
err: writeExceptionWithMessage(
|
||||
121,
|
||||
`Document failed validation`,
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped write exception duplicate key",
|
||||
err: fmt.Errorf(
|
||||
"failed to create user: %w",
|
||||
writeExceptionWithMessage(
|
||||
11000,
|
||||
`E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }`,
|
||||
),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "bulk write exception duplicate key",
|
||||
err: bulkWriteExceptionWithMessage(
|
||||
11000,
|
||||
`E11000 duplicate key error collection: tm.customers index: phone_idx dup key: { phone: "+1234567890" }`,
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string fallback duplicate key",
|
||||
err: errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }] `),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := IsDuplicateKeyError(tt.err); got != tt.want {
|
||||
t.Fatalf("IsDuplicateKeyError() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateKeyMatchesField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const phoneMsg = `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`
|
||||
const usernameMsg = `E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }`
|
||||
const emailMsg = `E11000 duplicate key error collection: tm.customers index: email_1 dup key: { email: "user@example.com" }`
|
||||
const phoneValueContainsNameMsg = `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "username-like-value" }`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
field string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
field: "phone",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty field",
|
||||
err: writeExceptionWithMessage(11000, phoneMsg),
|
||||
field: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "phone duplicate matches phone field",
|
||||
err: writeExceptionWithMessage(11000, phoneMsg),
|
||||
field: "phone",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "phone duplicate does not match email field",
|
||||
err: writeExceptionWithMessage(11000, phoneMsg),
|
||||
field: "email",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "username duplicate matches username field",
|
||||
err: writeExceptionWithMessage(11000, usernameMsg),
|
||||
field: "username",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "username duplicate does not match name substring",
|
||||
err: writeExceptionWithMessage(11000, usernameMsg),
|
||||
field: "name",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "email duplicate matches email field via default index suffix",
|
||||
err: writeExceptionWithMessage(11000, emailMsg),
|
||||
field: "email",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "dup key value substring does not match unrelated field",
|
||||
err: writeExceptionWithMessage(11000, phoneValueContainsNameMsg),
|
||||
field: "name",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped duplicate key matches field",
|
||||
err: fmt.Errorf(
|
||||
"failed to register customer: %w",
|
||||
writeExceptionWithMessage(11000, phoneMsg),
|
||||
),
|
||||
field: "phone",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non duplicate write error does not match",
|
||||
err: writeExceptionWithMessage(121, phoneMsg),
|
||||
field: "phone",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated error does not match",
|
||||
err: errors.New("timeout"),
|
||||
field: "phone",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "string fallback duplicate key matches field",
|
||||
err: errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }] `),
|
||||
field: "phone",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string fallback duplicate key does not match unrelated field",
|
||||
err: errors.New(`write exception: write errors: [E11000 duplicate key error collection: tm.users index: username_idx dup key: { username: "admin" }] `),
|
||||
field: "name",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := DuplicateKeyMatchesField(tt.err, tt.field); got != tt.want {
|
||||
t.Fatalf("DuplicateKeyMatchesField(%q) = %v, want %v", tt.field, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDuplicateIndexName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "phone index",
|
||||
message: `E11000 duplicate key error collection: tm.users index: phone_idx dup key: { phone: "+1234567890" }`,
|
||||
want: "phone_idx",
|
||||
},
|
||||
{
|
||||
name: "missing index segment",
|
||||
message: `E11000 duplicate key error collection: tm.users dup key: { phone: "+1234567890" }`,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := extractDuplicateIndexName(tt.message); got != tt.want {
|
||||
t.Fatalf("extractDuplicateIndexName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDuplicateKeyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "single field",
|
||||
message: `index: phone_idx dup key: { phone: "+1234567890" }`,
|
||||
want: []string{"phone"},
|
||||
},
|
||||
{
|
||||
name: "compound dup key",
|
||||
message: `index: user_company_idx dup key: { user_id: ObjectId('507f1f77bcf86cd799439011'), company_id: ObjectId('507f1f77bcf86cd799439012') }`,
|
||||
want: []string{"user_id", "company_id"},
|
||||
},
|
||||
{
|
||||
name: "value containing colon does not add false field",
|
||||
message: `index: phone_idx dup key: { phone: "office:123" }`,
|
||||
want: []string{"phone"},
|
||||
},
|
||||
{
|
||||
name: "missing dup key segment",
|
||||
message: `index: phone_idx`,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := extractDuplicateKeyFields(tt.message)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("extractDuplicateKeyFields() = %v, want %v", got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("extractDuplicateKeyFields() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexNameMatchesField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
indexName string
|
||||
field string
|
||||
want bool
|
||||
}{
|
||||
{indexName: "phone_idx", field: "phone", want: true},
|
||||
{indexName: "phone_1", field: "phone", want: true},
|
||||
{indexName: "phone", field: "phone", want: true},
|
||||
{indexName: "username_idx", field: "name", want: false},
|
||||
{indexName: "email_idx", field: "mail", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.indexName+"_"+tt.field, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := indexNameMatchesField(tt.indexName, tt.field); got != tt.want {
|
||||
t.Fatalf("indexNameMatchesField(%q, %q) = %v, want %v", tt.indexName, tt.field, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeExceptionWithMessage(code int, message string) error {
|
||||
return mongodriver.WriteException{
|
||||
WriteErrors: []mongodriver.WriteError{
|
||||
{Code: code, Message: message},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func bulkWriteExceptionWithMessage(code int, message string) error {
|
||||
return mongodriver.BulkWriteException{
|
||||
WriteErrors: []mongodriver.BulkWriteError{
|
||||
{WriteError: mongodriver.WriteError{Code: code, Message: message}},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user