From 5c2eed838343610e26f33d60be57d65b53899582 Mon Sep 17 00:00:00 2001 From: Saumil Diwaker Date: Wed, 3 Sep 2025 20:29:29 +0530 Subject: [PATCH 1/3] Many-to-many association tests with edge cases --- tests/associations_many2many_test.go | 156 +++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/associations_many2many_test.go b/tests/associations_many2many_test.go index 1c9cc51..1431e88 100644 --- a/tests/associations_many2many_test.go +++ b/tests/associations_many2many_test.go @@ -461,3 +461,159 @@ func TestMany2ManyDuplicateBelongsToAssociation(t *testing.T) { tests.AssertEqual(t, nil, err) tests.AssertEqual(t, user2, findUser2) } + +func TestMany2ManyEmptyAssociations(t *testing.T) { + user := User{Name: "TestEmptyAssociations"} + + // Create user with no associations + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create user: %v", err) + } + + var languages []Language + if err := DB.Model(&user).Association("Languages").Find(&languages); err != nil { + t.Errorf("Error finding empty associations: %v", err) + } + + if len(languages) != 0 { + t.Errorf("Expected 0 languages, got %d", len(languages)) + } + + count := DB.Model(&user).Association("Languages").Count() + if count != 0 { + t.Errorf("Expected count 0 for empty association, got %d", count) + } + + if err := DB.Model(&user).Association("Languages").Clear(); err != nil { + t.Errorf("Error clearing empty association: %v", err) + } + + if err := DB.Model(&user).Association("Languages").Delete(&Language{}); err != nil { + t.Errorf("Error deleting from empty association: %v", err) + } +} + +func TestMany2ManyAssociationCountValidation(t *testing.T) { + user := *GetUser("count-validation", Config{Languages: 3}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create: %v", err) + } + + // Initial count check + initialCount := DB.Model(&user).Association("Languages").Count() + if initialCount != 3 { + t.Fatalf("Expected initial count 3, got %d", initialCount) + } + + newLanguages := []Language{ + {Code: "count-test-1", Name: "Count Test 1"}, + {Code: "count-test-2", Name: "Count Test 2"}, + } + DB.Create(&newLanguages) + + if err := DB.Model(&user).Association("Languages").Append(&newLanguages); err != nil { + t.Fatalf("Error appending languages: %v", err) + } + + // Check count after append + countAfterAppend := DB.Model(&user).Association("Languages").Count() + if countAfterAppend != 5 { + t.Errorf("Expected count 5 after append, got %d", countAfterAppend) + } + + replaceLanguage := Language{Code: "count-replace", Name: "Count Replace"} + DB.Create(&replaceLanguage) + + if err := DB.Model(&user).Association("Languages").Replace(&replaceLanguage); err != nil { + t.Fatalf("Error replacing languages: %v", err) + } + + // Check count after replace + countAfterReplace := DB.Model(&user).Association("Languages").Count() + if countAfterReplace != 1 { + t.Errorf("Expected count 1 after replace, got %d", countAfterReplace) + } + + // Verify actual data matches count + var actualLanguages []Language + DB.Model(&user).Association("Languages").Find(&actualLanguages) + if len(actualLanguages) != int(countAfterReplace) { + t.Errorf("Count mismatch: Count() returned %d but Find() returned %d languages", + countAfterReplace, len(actualLanguages)) + } +} + +func TestMany2ManyConstraintViolations(t *testing.T) { + user := *GetUser("constraint-test", Config{Languages: 1}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("errors happened when create: %v", err) + } + + existingLanguage := user.Languages[0] + + // append the same language again + if err := DB.Model(&user).Association("Languages").Append(&existingLanguage); err != nil { + t.Logf("Appending duplicate language resulted in: %v", err) + } + + // Verify count is still correct + count := DB.Model(&user).Association("Languages").Count() + if count > 1 { + t.Errorf("Expected count 1 after duplicate append, got %d", count) + } + + tempLanguage := Language{Code: "temp-invalid", Name: "Temp Invalid"} + if err := DB.Create(&tempLanguage).Error; err != nil { + t.Logf("Could not create temp language for FK test: %v", err) + return + } + + // append, then delete the language record to create inconsistency + if err := DB.Model(&user).Association("Languages").Append(&tempLanguage); err != nil { + t.Logf("Could not append temp language: %v", err) + } + + // Delete the language record directly + DB.Unscoped().Delete(&tempLanguage) + + // access associations after deleting referenced record + var languages []Language + if err := DB.Model(&user).Association("Languages").Find(&languages); err != nil { + t.Logf("Finding associations after FK deletion resulted in: %v", err) + } + + var manyLanguages []Language + for i := 0; i < 10; i++ { + manyLanguages = append(manyLanguages, Language{ + Code: fmt.Sprintf("mass-test-%d", i), + Name: fmt.Sprintf("Mass Test %d", i), + }) + } + + // Create the languages first + if err := DB.Create(&manyLanguages).Error; err != nil { + t.Logf("Creating many languages failed: %v", err) + return + } + + // append all at once + if err := DB.Model(&user).Association("Languages").Append(&manyLanguages); err != nil { + t.Logf("Mass append operation resulted in: %v", err) + } + + // Verify the operation completed + finalCount := DB.Model(&user).Association("Languages").Count() + t.Logf("Final language count after mass operation: %d", finalCount) + + if err := DB.Model(&user).Association("Languages").Clear(); err != nil { + t.Errorf("Error clearing associations after mass operation: %v", err) + } + + // Verify clear worked + countAfterClear := DB.Model(&user).Association("Languages").Count() + if countAfterClear != 0 { + t.Errorf("Expected count 0 after clear, got %d", countAfterClear) + } +} From e7d94b89a345a1c67117633dcab2cf59db25f9cf Mon Sep 17 00:00:00 2001 From: Saumil Diwaker Date: Wed, 3 Sep 2025 20:42:48 +0530 Subject: [PATCH 2/3] add basic association tests for BelongsTo, HasMany, and ManyToMany --- tests/associations_test.go | 155 ++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/tests/associations_test.go b/tests/associations_test.go index 1db4f2f..50ef99a 100644 --- a/tests/associations_test.go +++ b/tests/associations_test.go @@ -367,8 +367,8 @@ func TestAssociationEmptyQueryClause(t *testing.T) { Organizations []Organization `gorm:"many2many:region_orgs;"` } type RegionOrg struct { - RegionId uint - OrganizationId uint + RegionID uint + OrganizationID uint Empty myType } if err := DB.SetupJoinTable(&Region{}, "Organizations", &RegionOrg{}); err != nil { @@ -394,3 +394,154 @@ func TestAssociationEmptyQueryClause(t *testing.T) { tests.AssertEqual(t, len(orgs), 0) } } + +func TestBasicBelongsToAssociation(t *testing.T) { + // Test basic BelongsTo association operations + user := GetUser("TestBelongsTo", Config{Company: true}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user with company: %v", err) + } + + // finding the association + var foundUser User + if err := DB.Preload("Company").First(&foundUser, "\"id\" = ?", user.ID).Error; err != nil { + t.Fatalf("Failed to find user with company: %v", err) + } + + if foundUser.Company.ID != user.Company.ID { + t.Fatalf("Company ID mismatch: expected %d, got %d", user.Company.ID, foundUser.Company.ID) + } + + // association count + AssertAssociationCount(t, user, "Company", 1, "after creation") + + // replacing association + newCompany := Company{Name: "New Test Company"} + if err := DB.Create(&newCompany).Error; err != nil { + t.Fatalf("Failed to create new company: %v", err) + } + + if err := DB.Model(&user).Association("Company").Replace(&newCompany); err != nil { + t.Fatalf("Failed to replace company association: %v", err) + } + + var updatedUser User + if err := DB.Preload("Company").First(&updatedUser, "\"id\" = ?", user.ID).Error; err != nil { + t.Fatalf("Failed to find updated user: %v", err) + } + + if updatedUser.Company.ID != newCompany.ID { + t.Fatalf("Company was not replaced: expected %d, got %d", newCompany.ID, updatedUser.Company.ID) + } +} + +func TestBasicHasManyAssociation(t *testing.T) { + // Test basic HasMany association operations + user := GetUser("TestHasMany", Config{Pets: 3}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user with pets: %v", err) + } + + // association count + AssertAssociationCount(t, user, "Pets", 3, "after creation") + + // finding pets + var pets []Pet + if err := DB.Model(&user).Association("Pets").Find(&pets); err != nil { + t.Fatalf("Failed to find pets: %v", err) + } + + if len(pets) != 3 { + t.Fatalf("Expected 3 pets, got %d", len(pets)) + } + + // appending new pet + newPet := Pet{Name: "Additional Pet", UserID: &user.ID} + if err := DB.Model(&user).Association("Pets").Append(&newPet); err != nil { + t.Fatalf("Failed to append pet: %v", err) + } + + AssertAssociationCount(t, user, "Pets", 4, "after append") + + // deleting one pet from association + if err := DB.Model(&user).Association("Pets").Delete(&pets[0]); err != nil { + t.Fatalf("Failed to delete pet from association: %v", err) + } + + AssertAssociationCount(t, user, "Pets", 3, "after delete") +} + +func TestBasicManyToManyAssociation(t *testing.T) { + // Test basic ManyToMany association operations + user := GetUser("TestManyToMany", Config{Languages: 2}) + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user with languages: %v", err) + } + + // association count + AssertAssociationCount(t, user, "Languages", 2, "after creation") + + // finding languages + var languages []Language + if err := DB.Model(&user).Association("Languages").Find(&languages); err != nil { + t.Fatalf("Failed to find languages: %v", err) + } + + if len(languages) != 2 { + t.Fatalf("Expected 2 languages, got %d", len(languages)) + } + + // appending new language + newLanguage := Language{Code: "FR", Name: "French"} + if err := DB.Create(&newLanguage).Error; err != nil { + t.Fatalf("Failed to create new language: %v", err) + } + + if err := DB.Model(&user).Association("Languages").Append(&newLanguage); err != nil { + t.Fatalf("Failed to append language: %v", err) + } + + AssertAssociationCount(t, user, "Languages", 3, "after append") + + // replacing all languages + replaceLanguages := []Language{ + {Code: "DE", Name: "German"}, + {Code: "IT", Name: "Italian"}, + } + + for i := range replaceLanguages { + if err := DB.Create(&replaceLanguages[i]).Error; err != nil { + t.Fatalf("Failed to create replacement language: %v", err) + } + } + + if err := DB.Model(&user).Association("Languages").Replace(replaceLanguages); err != nil { + t.Fatalf("Failed to replace languages: %v", err) + } + + AssertAssociationCount(t, user, "Languages", 2, "after replace") + + var finalLanguages []Language + if err := DB.Model(&user).Association("Languages").Find(&finalLanguages); err != nil { + t.Fatalf("Failed to find final languages: %v", err) + } + + languageCodes := make(map[string]bool) + for _, lang := range finalLanguages { + languageCodes[lang.Code] = true + } + + if !languageCodes["DE"] || !languageCodes["IT"] { + t.Fatal("Languages were not replaced correctly") + } + + // clearing all associations + if err := DB.Model(&user).Association("Languages").Clear(); err != nil { + t.Fatalf("Failed to clear languages: %v", err) + } + + AssertAssociationCount(t, user, "Languages", 0, "after clear") +} From dc3a89adb9f6d07e1652b89b00421730a5d7bcda Mon Sep 17 00:00:00 2001 From: Saumil Diwaker Date: Fri, 5 Sep 2025 18:23:27 +0530 Subject: [PATCH 3/3] Add missing checks --- tests/associations_many2many_test.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/associations_many2many_test.go b/tests/associations_many2many_test.go index 1431e88..a70e1d4 100644 --- a/tests/associations_many2many_test.go +++ b/tests/associations_many2many_test.go @@ -558,7 +558,7 @@ func TestMany2ManyConstraintViolations(t *testing.T) { t.Logf("Appending duplicate language resulted in: %v", err) } - // Verify count is still correct + // Verify count is still correct after duplicate append attempt count := DB.Model(&user).Association("Languages").Count() if count > 1 { t.Errorf("Expected count 1 after duplicate append, got %d", count) @@ -584,6 +584,10 @@ func TestMany2ManyConstraintViolations(t *testing.T) { t.Logf("Finding associations after FK deletion resulted in: %v", err) } + // Get count before mass operation for verification + countBeforeMass := DB.Model(&user).Association("Languages").Count() + t.Logf("Language count before mass operation: %d", countBeforeMass) + var manyLanguages []Language for i := 0; i < 10; i++ { manyLanguages = append(manyLanguages, Language{ @@ -603,9 +607,25 @@ func TestMany2ManyConstraintViolations(t *testing.T) { t.Logf("Mass append operation resulted in: %v", err) } - // Verify the operation completed + // Verify the operation completed with proper count validation finalCount := DB.Model(&user).Association("Languages").Count() - t.Logf("Final language count after mass operation: %d", finalCount) + // Should have at least the previous count + 10 new languages + expectedMinCount := countBeforeMass + 10 + + t.Logf("Final language count after mass operation: %d (expected at least %d)", finalCount, expectedMinCount) + + if finalCount < expectedMinCount { + t.Errorf("Expected at least %d languages after mass append, got %d", expectedMinCount, finalCount) + } + + var actualLanguages []Language + if err := DB.Model(&user).Association("Languages").Find(&actualLanguages); err == nil { + actualCount := len(actualLanguages) + if actualCount != int(finalCount) { + t.Errorf("Count mismatch: Count() returned %d but Find() returned %d languages", + finalCount, actualCount) + } + } if err := DB.Model(&user).Association("Languages").Clear(); err != nil { t.Errorf("Error clearing associations after mass operation: %v", err)