forked from baron/baron-sso
313 lines
10 KiB
Go
313 lines
10 KiB
Go
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
|
|
}
|