diff --git a/internal/customer/errors.go b/internal/customer/errors.go index 28c0e8d..ad99cbb 100644 --- a/internal/customer/errors.go +++ b/internal/customer/errors.go @@ -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 + } +} diff --git a/internal/customer/handler.go b/internal/customer/handler.go index 5431089..a499f8e 100644 --- a/internal/customer/handler.go +++ b/internal/customer/handler.go @@ -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") { diff --git a/internal/customer/repository.go b/internal/customer/repository.go index ed52842..cc557d4 100644 --- a/internal/customer/repository.go +++ b/internal/customer/repository.go @@ -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} diff --git a/internal/customer/service.go b/internal/customer/service.go index a75b4fc..8818064 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -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, diff --git a/internal/user/errors.go b/internal/user/errors.go index 2b9a669..1155b1b 100644 --- a/internal/user/errors.go +++ b/internal/user/errors.go @@ -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 + } +} diff --git a/internal/user/handler.go b/internal/user/handler.go index f2c5960..8211152 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -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(), "") } diff --git a/internal/user/repository.go b/internal/user/repository.go index 09796d3..e758a4b 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -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}) diff --git a/internal/user/service.go b/internal/user/service.go index fbfac0b..b852df9 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -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, diff --git a/pkg/mongo/duplicate.go b/pkg/mongo/duplicate.go new file mode 100644 index 0000000..a45b18c --- /dev/null +++ b/pkg/mongo/duplicate.go @@ -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 +} diff --git a/pkg/mongo/duplicate_test.go b/pkg/mongo/duplicate_test.go new file mode 100644 index 0000000..d81ecdb --- /dev/null +++ b/pkg/mongo/duplicate_test.go @@ -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}}, + }, + } +}