package repository import ( "baron-sso-backend/internal/domain" "context" "errors" "fmt" "testing" "time" "github.com/google/uuid" "github.com/lib/pq" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestCheckDataIntegrityDetectsTenantAndUserProblems(t *testing.T) { ctx := context.Background() suffix := uuid.NewString() parent := domain.Tenant{ ID: uuid.NewString(), Name: "Deleted Parent " + suffix, Slug: "deleted-parent-" + suffix, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, } child := domain.Tenant{ ID: uuid.NewString(), Name: "Orphan Child " + suffix, Slug: "orphan-child-" + suffix, Type: domain.TenantTypeOrganization, ParentID: &parent.ID, Status: domain.TenantStatusActive, } dupA := domain.Tenant{ ID: uuid.NewString(), Name: "Duplicate A " + suffix, Slug: "Dup-" + suffix, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, } dupB := domain.Tenant{ ID: uuid.NewString(), Name: "Duplicate B " + suffix, Slug: "dup-" + suffix, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, } require.NoError(t, testDB.Create(&parent).Error) require.NoError(t, testDB.Create(&child).Error) require.NoError(t, testDB.Create(&dupA).Error) require.NoError(t, testDB.Create(&dupB).Error) require.NoError(t, testDB.Delete(&domain.Tenant{}, "id = ?", parent.ID).Error) orphanUser := domain.User{ ID: uuid.NewString(), Email: "orphan-" + suffix + "@example.com", Name: "Orphan User", Role: domain.RoleUser, TenantID: &parent.ID, Status: domain.UserStatusActive, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } deletedLoginUser := domain.User{ ID: uuid.NewString(), Email: "deleted-login-user-" + suffix + "@example.com", Name: "Deleted Login User", Role: domain.RoleUser, TenantID: &child.ID, Status: domain.UserStatusActive, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } require.NoError(t, testDB.Create(&orphanUser).Error) require.NoError(t, testDB.Create(&deletedLoginUser).Error) require.NoError(t, testDB.Create(&domain.UserLoginID{ ID: uuid.NewString(), UserID: orphanUser.ID, TenantID: parent.ID, FieldKey: "emp_id", LoginID: "EMP-" + suffix, }).Error) require.NoError(t, testDB.Create(&domain.UserLoginID{ ID: uuid.NewString(), UserID: deletedLoginUser.ID, TenantID: child.ID, FieldKey: "emp_id", LoginID: "MISSING-" + suffix, }).Error) require.NoError(t, testDB.Delete(&domain.User{}, "id = ?", deletedLoginUser.ID).Error) report, err := CheckDataIntegrity(ctx, testDB) require.NoError(t, err) require.Equal(t, domain.DataIntegrityStatusFail, report.Status) require.Equal(t, int64(5), report.Summary.Failures) // Reverted back to 5 due to successful soft delete simulation requireIntegrityCheck(t, report, "tenant_integrity", "duplicate_tenant_slugs", domain.DataIntegrityStatusFail, 1) requireIntegrityCheck(t, report, "tenant_integrity", "orphan_tenant_parents", domain.DataIntegrityStatusFail, 1) requireIntegrityCheck(t, report, "user_integrity", "orphan_user_tenant_memberships", domain.DataIntegrityStatusFail, 1) requireIntegrityCheck(t, report, "user_integrity", "orphan_user_login_id_tenants", domain.DataIntegrityStatusFail, 1) requireIntegrityCheck(t, report, "user_integrity", "orphan_user_login_id_users", domain.DataIntegrityStatusFail, 1) } func TestCheckDataIntegrityDetectsHardOrphanUserLoginIDRows(t *testing.T) { ctx := context.Background() suffix := uuid.NewString() rollback := errors.New("rollback hard orphan fixture") err := testDB.Transaction(func(tx *gorm.DB) error { var constraintNames []string if err := tx.Raw(` SELECT conname FROM pg_constraint WHERE conrelid = 'user_login_ids'::regclass AND contype = 'f' `).Scan(&constraintNames).Error; err != nil { return err } for _, constraintName := range constraintNames { statement := fmt.Sprintf("ALTER TABLE user_login_ids DROP CONSTRAINT %s", pq.QuoteIdentifier(constraintName)) if err := tx.Exec(statement).Error; err != nil { return err } } before, err := CheckDataIntegrity(ctx, tx) if err != nil { return err } beforeTenantCount, err := integrityCheckCount(before, "user_integrity", "orphan_user_login_id_tenants") if err != nil { return err } beforeUserCount, err := integrityCheckCount(before, "user_integrity", "orphan_user_login_id_users") if err != nil { return err } if err := tx.Create(&domain.UserLoginID{ ID: uuid.NewString(), UserID: uuid.NewString(), TenantID: uuid.NewString(), FieldKey: "emp_id", LoginID: "HARD-ORPHAN-" + suffix, }).Error; err != nil { return err } report, err := CheckDataIntegrity(ctx, tx) if err != nil { return err } if err := expectIntegrityCheck(report, "user_integrity", "orphan_user_login_id_tenants", domain.DataIntegrityStatusFail, beforeTenantCount+1); err != nil { return err } if err := expectIntegrityCheck(report, "user_integrity", "orphan_user_login_id_users", domain.DataIntegrityStatusFail, beforeUserCount+1); err != nil { return err } return rollback }) require.ErrorIs(t, err, rollback) } func TestListAndDeleteOrphanUserLoginIDsOnlyDeletesRevalidatedTargets(t *testing.T) { ctx := context.Background() suffix := uuid.NewString() validTenant := domain.Tenant{ ID: uuid.NewString(), Name: "Valid Tenant " + suffix, Slug: "valid-tenant-" + suffix, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, } deletedTenant := domain.Tenant{ ID: uuid.NewString(), Name: "Deleted Tenant " + suffix, Slug: "deleted-tenant-" + suffix, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, } require.NoError(t, testDB.Create(&validTenant).Error) require.NoError(t, testDB.Create(&deletedTenant).Error) validUser := domain.User{ ID: uuid.NewString(), Email: "valid-login-" + suffix + "@example.com", Name: "Valid Login User", Role: domain.RoleUser, TenantID: &validTenant.ID, Status: domain.UserStatusActive, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } deletedUser := domain.User{ ID: uuid.NewString(), Email: "deleted-login-" + suffix + "@example.com", Name: "Deleted Login User", Role: domain.RoleUser, TenantID: &validTenant.ID, Status: domain.UserStatusActive, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } require.NoError(t, testDB.Create(&validUser).Error) require.NoError(t, testDB.Create(&deletedUser).Error) validLogin := domain.UserLoginID{ ID: uuid.NewString(), UserID: validUser.ID, TenantID: validTenant.ID, FieldKey: "emp_id", LoginID: "VALID-" + suffix, } deletedTenantLogin := domain.UserLoginID{ ID: uuid.NewString(), UserID: validUser.ID, TenantID: deletedTenant.ID, FieldKey: "emp_id", LoginID: "DELETED-TENANT-" + suffix, } deletedUserLogin := domain.UserLoginID{ ID: uuid.NewString(), UserID: deletedUser.ID, TenantID: validTenant.ID, FieldKey: "emp_id", LoginID: "DELETED-USER-" + suffix, } require.NoError(t, testDB.Create(&validLogin).Error) require.NoError(t, testDB.Create(&deletedTenantLogin).Error) require.NoError(t, testDB.Create(&deletedUserLogin).Error) require.NoError(t, testDB.Delete(&domain.Tenant{}, "id = ?", deletedTenant.ID).Error) require.NoError(t, testDB.Delete(&domain.User{}, "id = ?", deletedUser.ID).Error) items, err := ListOrphanUserLoginIDs(ctx, testDB, nil) require.NoError(t, err) orphanReasons := map[string][]string{} for _, item := range items { orphanReasons[item.ID] = item.Reasons } require.Equal(t, []string{"deleted_tenant"}, orphanReasons[deletedTenantLogin.ID]) require.Equal(t, []string{"deleted_user"}, orphanReasons[deletedUserLogin.ID]) require.NotContains(t, orphanReasons, validLogin.ID) result, err := DeleteOrphanUserLoginIDs(ctx, testDB, []string{ deletedTenantLogin.ID, validLogin.ID, "00000000-0000-0000-0000-000000000000", }) require.NoError(t, err) require.Equal(t, int64(1), result.DeletedCount) require.Len(t, result.Deleted, 1) require.Equal(t, deletedTenantLogin.ID, result.Deleted[0].ID) require.ElementsMatch(t, []string{ validLogin.ID, "00000000-0000-0000-0000-000000000000", }, result.SkippedIDs) var deletedTenantLoginCount int64 require.NoError(t, testDB.Model(&domain.UserLoginID{}).Where("id = ?", deletedTenantLogin.ID).Count(&deletedTenantLoginCount).Error) require.Equal(t, int64(0), deletedTenantLoginCount) var validLoginCount int64 require.NoError(t, testDB.Model(&domain.UserLoginID{}).Where("id = ?", validLogin.ID).Count(&validLoginCount).Error) require.Equal(t, int64(1), validLoginCount) } func requireIntegrityCheck(t *testing.T, report domain.DataIntegrityReport, sectionKey, checkKey string, status domain.DataIntegrityStatus, count int64) { t.Helper() require.NoError(t, expectIntegrityCheck(report, sectionKey, checkKey, status, count)) } func expectIntegrityCheck(report domain.DataIntegrityReport, sectionKey, checkKey string, status domain.DataIntegrityStatus, count int64) error { check, ok := findIntegrityCheck(report, sectionKey, checkKey) if !ok { return fmt.Errorf("integrity check %s/%s not found", sectionKey, checkKey) } if check.Status != status { return fmt.Errorf("integrity check %s/%s status = %s, want %s", sectionKey, checkKey, check.Status, status) } if check.Count != count { return fmt.Errorf("integrity check %s/%s count = %d, want %d", sectionKey, checkKey, check.Count, count) } return nil } func integrityCheckCount(report domain.DataIntegrityReport, sectionKey, checkKey string) (int64, error) { check, ok := findIntegrityCheck(report, sectionKey, checkKey) if !ok { return 0, fmt.Errorf("integrity check %s/%s not found", sectionKey, checkKey) } return check.Count, nil } func findIntegrityCheck(report domain.DataIntegrityReport, sectionKey, checkKey string) (domain.DataIntegrityCheck, bool) { for _, section := range report.Sections { if section.Key != sectionKey { continue } for _, check := range section.Checks { if check.Key == checkKey { return check, true } } } return domain.DataIntegrityCheck{}, false }