forked from baron/baron-sso
네이버 계정 정합성 맞춤
This commit is contained in:
@@ -63,7 +63,7 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
// Auto-migrate
|
||||
err = db.AutoMigrate(&domain.Tenant{}, &domain.TenantDomain{}, &domain.User{}, &domain.UserLoginID{}, &domain.UserProjectionState{}, &domain.ClientConsent{}, &domain.RPUserMetadata{}, &domain.RPUsageEvent{}, &domain.KetoOutbox{}, &domain.WorksmobileOutbox{})
|
||||
err = db.AutoMigrate(&domain.Tenant{}, &domain.TenantDomain{}, &domain.User{}, &domain.UserLoginID{}, &domain.ClientConsent{}, &domain.RPUserMetadata{}, &domain.RPUsageEvent{}, &domain.KetoOutbox{}, &domain.WorksmobileOutbox{})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to migrate database: %s", err)
|
||||
}
|
||||
|
||||
@@ -26,26 +26,76 @@ WHERE u.deleted_at IS NULL
|
||||
}
|
||||
|
||||
func ClearOrphanUserTenantMemberships(ctx context.Context, db *gorm.DB) (int64, error) {
|
||||
result := db.WithContext(ctx).Exec(`
|
||||
userResult := db.WithContext(ctx).Exec(`
|
||||
WITH orphan_users AS (
|
||||
SELECT u.id
|
||||
SELECT u.id AS user_id,
|
||||
replacement.id AS replacement_tenant_id
|
||||
FROM users AS u
|
||||
WHERE u.deleted_at IS NULL
|
||||
AND (
|
||||
u.tenant_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM tenants AS t
|
||||
WHERE t.id = u.tenant_id
|
||||
AND t.deleted_at IS NULL
|
||||
JOIN tenants AS deleted_tenant
|
||||
ON deleted_tenant.id = u.tenant_id
|
||||
AND deleted_tenant.deleted_at IS NOT NULL
|
||||
JOIN LATERAL (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT parent.id, parent.parent_id, parent.deleted_at, 1 AS depth
|
||||
FROM tenants AS parent
|
||||
WHERE parent.id = deleted_tenant.parent_id
|
||||
UNION ALL
|
||||
SELECT parent.id, parent.parent_id, parent.deleted_at, ancestors.depth + 1
|
||||
FROM tenants AS parent
|
||||
JOIN ancestors ON parent.id = ancestors.parent_id
|
||||
WHERE ancestors.parent_id IS NOT NULL
|
||||
AND ancestors.parent_id <> ancestors.id
|
||||
)
|
||||
)
|
||||
SELECT id
|
||||
FROM ancestors
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY depth
|
||||
LIMIT 1
|
||||
) AS replacement ON true
|
||||
WHERE u.deleted_at IS NULL
|
||||
AND u.tenant_id IS NOT NULL
|
||||
)
|
||||
UPDATE users AS u
|
||||
SET tenant_id = NULL,
|
||||
SET tenant_id = ou.replacement_tenant_id,
|
||||
updated_at = NOW()
|
||||
FROM orphan_users AS ou
|
||||
WHERE u.id = ou.id
|
||||
WHERE u.id = ou.user_id
|
||||
`)
|
||||
return result.RowsAffected, result.Error
|
||||
if userResult.Error != nil {
|
||||
return userResult.RowsAffected, userResult.Error
|
||||
}
|
||||
|
||||
loginResult := db.WithContext(ctx).Exec(`
|
||||
WITH orphan_login_ids AS (
|
||||
SELECT uli.id AS login_id,
|
||||
replacement.id AS replacement_tenant_id
|
||||
FROM user_login_ids AS uli
|
||||
JOIN tenants AS deleted_tenant
|
||||
ON deleted_tenant.id = uli.tenant_id
|
||||
AND deleted_tenant.deleted_at IS NOT NULL
|
||||
JOIN LATERAL (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT parent.id, parent.parent_id, parent.deleted_at, 1 AS depth
|
||||
FROM tenants AS parent
|
||||
WHERE parent.id = deleted_tenant.parent_id
|
||||
UNION ALL
|
||||
SELECT parent.id, parent.parent_id, parent.deleted_at, ancestors.depth + 1
|
||||
FROM tenants AS parent
|
||||
JOIN ancestors ON parent.id = ancestors.parent_id
|
||||
WHERE ancestors.parent_id IS NOT NULL
|
||||
AND ancestors.parent_id <> ancestors.id
|
||||
)
|
||||
SELECT id
|
||||
FROM ancestors
|
||||
WHERE deleted_at IS NULL
|
||||
ORDER BY depth
|
||||
LIMIT 1
|
||||
) AS replacement ON true
|
||||
)
|
||||
UPDATE user_login_ids AS uli
|
||||
SET tenant_id = oli.replacement_tenant_id
|
||||
FROM orphan_login_ids AS oli
|
||||
WHERE uli.id = oli.login_id
|
||||
`)
|
||||
return userResult.RowsAffected + loginResult.RowsAffected, loginResult.Error
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestClearOrphanUserTenantMemberships(t *testing.T) {
|
||||
require.NoError(t, testDB.Unscoped().Where("slug IN ?", []string{"orphan-active", "orphan-deleted"}).Delete(&domain.Tenant{}).Error)
|
||||
|
||||
activeTenant := &domain.Tenant{Name: "Active Tenant", Slug: "orphan-active", Type: domain.TenantTypeCompany}
|
||||
deletedTenant := &domain.Tenant{Name: "Deleted Tenant", Slug: "orphan-deleted", Type: domain.TenantTypeCompany}
|
||||
deletedTenant := &domain.Tenant{Name: "Deleted Tenant", Slug: "orphan-deleted", Type: domain.TenantTypeUserGroup, ParentID: &activeTenant.ID}
|
||||
require.NoError(t, tenantRepo.Create(ctx, activeTenant))
|
||||
require.NoError(t, tenantRepo.Create(ctx, deletedTenant))
|
||||
require.NoError(t, testDB.Delete(&domain.Tenant{}, "id = ?", deletedTenant.ID).Error)
|
||||
@@ -39,6 +39,13 @@ func TestClearOrphanUserTenantMemberships(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, activeUser))
|
||||
require.NoError(t, repo.Create(ctx, orphanUser))
|
||||
loginID := &domain.UserLoginID{
|
||||
UserID: orphanUser.ID,
|
||||
TenantID: deletedTenant.ID,
|
||||
FieldKey: "employee_number",
|
||||
LoginID: "orphan-membership-login",
|
||||
}
|
||||
require.NoError(t, testDB.Create(loginID).Error)
|
||||
|
||||
count, err := CountOrphanUserTenantMemberships(ctx, testDB)
|
||||
require.NoError(t, err)
|
||||
@@ -46,7 +53,7 @@ func TestClearOrphanUserTenantMemberships(t *testing.T) {
|
||||
|
||||
affected, err := ClearOrphanUserTenantMemberships(ctx, testDB)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), affected)
|
||||
assert.Equal(t, int64(2), affected)
|
||||
|
||||
foundActive, err := repo.FindByEmail(ctx, activeUser.Email)
|
||||
require.NoError(t, err)
|
||||
@@ -56,7 +63,12 @@ func TestClearOrphanUserTenantMemberships(t *testing.T) {
|
||||
|
||||
foundOrphan, err := repo.FindByEmail(ctx, orphanUser.Email)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, foundOrphan.TenantID)
|
||||
require.NotNil(t, foundOrphan.TenantID)
|
||||
assert.Equal(t, activeTenant.ID, *foundOrphan.TenantID)
|
||||
|
||||
var foundLogin domain.UserLoginID
|
||||
require.NoError(t, testDB.First(&foundLogin, "id = ?", loginID.ID).Error)
|
||||
assert.Equal(t, activeTenant.ID, foundLogin.TenantID)
|
||||
|
||||
count, err = CountOrphanUserTenantMemberships(ctx, testDB)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type UserProjectionRepository interface {
|
||||
IsReady(ctx context.Context) (bool, error)
|
||||
GetStatus(ctx context.Context) (domain.UserProjectionStatus, error)
|
||||
CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error)
|
||||
CountTenantMembersRecursive(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error)
|
||||
ReplaceAllFromKratos(ctx context.Context, users []domain.User) error
|
||||
MarkFailed(ctx context.Context, syncErr error) error
|
||||
}
|
||||
|
||||
type userProjectionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserProjectionRepository(db *gorm.DB) UserProjectionRepository {
|
||||
return &userProjectionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) IsReady(ctx context.Context) (bool, error) {
|
||||
status, err := r.GetStatus(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return status.Ready, nil
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) GetStatus(ctx context.Context) (domain.UserProjectionStatus, error) {
|
||||
var projectedUsers int64
|
||||
if err := r.db.WithContext(ctx).Model(&domain.User{}).Count(&projectedUsers).Error; err != nil {
|
||||
return domain.UserProjectionStatus{}, err
|
||||
}
|
||||
|
||||
var state domain.UserProjectionState
|
||||
err := r.db.WithContext(ctx).First(&state, "name = ?", domain.UserProjectionNameKratos).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return domain.UserProjectionStatus{
|
||||
Name: domain.UserProjectionNameKratos,
|
||||
Status: domain.UserProjectionStatusFailed,
|
||||
Ready: false,
|
||||
ProjectedUsers: projectedUsers,
|
||||
}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.UserProjectionStatus{}, err
|
||||
}
|
||||
return domain.UserProjectionStatus{
|
||||
Name: state.Name,
|
||||
Status: state.Status,
|
||||
Ready: state.Status == domain.UserProjectionStatusReady && state.LastSyncedAt != nil,
|
||||
LastSyncedAt: state.LastSyncedAt,
|
||||
LastError: state.LastError,
|
||||
UpdatedAt: &state.UpdatedAt,
|
||||
ProjectedUsers: projectedUsers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
|
||||
counts := make(map[string]int64, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
counts[tenant.ID] = 0
|
||||
}
|
||||
if len(tenants) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
valuePlaceholders := make([]string, 0, len(tenants))
|
||||
args := make([]any, 0, len(tenants)*2)
|
||||
for _, tenant := range tenants {
|
||||
valuePlaceholders = append(valuePlaceholders, "(?, ?)")
|
||||
args = append(args, strings.TrimSpace(tenant.ID), strings.TrimSpace(tenant.Slug))
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
WITH requested(tenant_id, slug) AS (
|
||||
VALUES %s
|
||||
)
|
||||
SELECT requested.tenant_id, COUNT(DISTINCT users.id) AS count
|
||||
FROM requested
|
||||
LEFT JOIN users ON users.deleted_at IS NULL AND (
|
||||
users.tenant_id::text = requested.tenant_id
|
||||
)
|
||||
GROUP BY requested.tenant_id
|
||||
`, strings.Join(valuePlaceholders, ","))
|
||||
|
||||
type result struct {
|
||||
TenantID string
|
||||
Count int64
|
||||
}
|
||||
var rows []result
|
||||
if err := r.db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
counts[row.TenantID] = row.Count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) CountTenantMembersRecursive(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
|
||||
counts := make(map[string]int64, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
counts[tenant.ID] = 0
|
||||
}
|
||||
if len(tenants) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
valuePlaceholders := make([]string, 0, len(tenants))
|
||||
args := make([]any, 0, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
valuePlaceholders = append(valuePlaceholders, "(?)")
|
||||
args = append(args, strings.TrimSpace(tenant.ID))
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
WITH RECURSIVE requested(tenant_id) AS (
|
||||
VALUES %s
|
||||
),
|
||||
descendants(root_tenant_id, tenant_id) AS (
|
||||
SELECT requested.tenant_id, requested.tenant_id
|
||||
FROM requested
|
||||
UNION ALL
|
||||
SELECT descendants.root_tenant_id, child.id::text
|
||||
FROM descendants
|
||||
JOIN tenants child
|
||||
ON child.parent_id::text = descendants.tenant_id
|
||||
AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT requested.tenant_id, COUNT(DISTINCT users.id) AS count
|
||||
FROM requested
|
||||
LEFT JOIN descendants
|
||||
ON descendants.root_tenant_id = requested.tenant_id
|
||||
LEFT JOIN users
|
||||
ON users.deleted_at IS NULL
|
||||
AND users.tenant_id::text = descendants.tenant_id
|
||||
GROUP BY requested.tenant_id
|
||||
`, strings.Join(valuePlaceholders, ","))
|
||||
|
||||
type result struct {
|
||||
TenantID string
|
||||
Count int64
|
||||
}
|
||||
var rows []result
|
||||
if err := r.db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
counts[row.TenantID] = row.Count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) ReplaceAllFromKratos(ctx context.Context, users []domain.User) error {
|
||||
now := time.Now()
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range users {
|
||||
users[i].DeletedAt = gorm.DeletedAt{}
|
||||
if users[i].CreatedAt.IsZero() {
|
||||
users[i].CreatedAt = now
|
||||
}
|
||||
if users[i].UpdatedAt.IsZero() {
|
||||
users[i].UpdatedAt = now
|
||||
}
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
// [FIX] Handle email conflicts before bulk upsert
|
||||
for _, u := range users {
|
||||
if u.Email != "" {
|
||||
// Hard-delete any record with same email but different ID to clear unique constraint
|
||||
_ = tx.Unscoped().Where("email = ? AND id != ?", u.Email, u.ID).Delete(&domain.User{}).Error
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
UpdateAll: true,
|
||||
}).Create(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return upsertUserProjectionState(tx, domain.UserProjectionStatusReady, &now, "")
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userProjectionRepository) MarkFailed(ctx context.Context, syncErr error) error {
|
||||
message := ""
|
||||
if syncErr != nil {
|
||||
message = syncErr.Error()
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return upsertUserProjectionState(tx, domain.UserProjectionStatusFailed, nil, message)
|
||||
})
|
||||
}
|
||||
|
||||
func upsertUserProjectionState(tx *gorm.DB, status string, syncedAt *time.Time, lastError string) error {
|
||||
state := domain.UserProjectionState{
|
||||
Name: domain.UserProjectionNameKratos,
|
||||
Status: status,
|
||||
LastSyncedAt: syncedAt,
|
||||
LastError: lastError,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "name"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"status",
|
||||
"last_synced_at",
|
||||
"last_error",
|
||||
"updated_at",
|
||||
}),
|
||||
}).Create(&state).Error
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserProjectionRepository_ReplaceAllFromKratosMarksReadyWithoutDeletingUsersMissingFromPartialList(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := NewUserProjectionRepository(testDB)
|
||||
|
||||
require.NoError(t, testDB.Exec("DELETE FROM user_projection_states").Error)
|
||||
require.NoError(t, testDB.Exec("DELETE FROM user_login_ids").Error)
|
||||
require.NoError(t, testDB.Exec("DELETE FROM users").Error)
|
||||
|
||||
tenantID := "10000000-0000-0000-0000-000000000001"
|
||||
tenantSlug := "projection-saman"
|
||||
require.NoError(t, testDB.Create(&domain.Tenant{
|
||||
ID: tenantID,
|
||||
Name: "Projection Saman",
|
||||
Slug: tenantSlug,
|
||||
Type: domain.TenantTypeCompany,
|
||||
Status: domain.TenantStatusActive,
|
||||
}).Error)
|
||||
existing := &domain.User{
|
||||
ID: "00000000-0000-0000-0000-000000000099",
|
||||
Email: "existing@example.com",
|
||||
Name: "Existing",
|
||||
CompanyCode: tenantSlug,
|
||||
TenantID: &tenantID,
|
||||
}
|
||||
require.NoError(t, NewUserRepository(testDB).Create(ctx, existing))
|
||||
|
||||
users := []domain.User{
|
||||
{
|
||||
ID: "00000000-0000-0000-0000-000000000101",
|
||||
Email: "one@example.com",
|
||||
Name: "One",
|
||||
CompanyCode: tenantSlug,
|
||||
TenantID: &tenantID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "00000000-0000-0000-0000-000000000102",
|
||||
Email: "two@example.com",
|
||||
Name: "Two",
|
||||
TenantID: &tenantID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, repo.ReplaceAllFromKratos(ctx, users))
|
||||
|
||||
ready, err := repo.IsReady(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ready)
|
||||
|
||||
counts, err := repo.CountTenantMembers(ctx, []domain.Tenant{
|
||||
{ID: tenantID, Slug: tenantSlug},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), counts[tenantID])
|
||||
|
||||
var activeCount int64
|
||||
require.NoError(t, testDB.Model(&domain.User{}).Count(&activeCount).Error)
|
||||
assert.Equal(t, int64(3), activeCount)
|
||||
|
||||
var existingCount int64
|
||||
require.NoError(t, testDB.Model(&domain.User{}).Where("id = ?", existing.ID).Count(&existingCount).Error)
|
||||
assert.Equal(t, int64(1), existingCount)
|
||||
|
||||
var existingRow domain.User
|
||||
require.NoError(t, testDB.Unscoped().First(&existingRow, "id = ?", existing.ID).Error)
|
||||
assert.False(t, existingRow.DeletedAt.Valid)
|
||||
}
|
||||
|
||||
func TestUserProjectionRepository_CountTenantMembersRecursiveIncludesDescendantsAndExcludesSoftDeletedUsers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := NewUserProjectionRepository(testDB)
|
||||
|
||||
parentID := "20000000-0000-0000-0000-000000000001"
|
||||
childID := "20000000-0000-0000-0000-000000000002"
|
||||
grandchildID := "20000000-0000-0000-0000-000000000003"
|
||||
siblingID := "20000000-0000-0000-0000-000000000004"
|
||||
tenantIDs := []string{parentID, childID, grandchildID, siblingID}
|
||||
|
||||
require.NoError(t, testDB.Exec("DELETE FROM user_login_ids").Error)
|
||||
require.NoError(t, testDB.Exec("DELETE FROM users").Error)
|
||||
require.NoError(t, testDB.Unscoped().Where("id IN ?", tenantIDs).Delete(&domain.Tenant{}).Error)
|
||||
|
||||
require.NoError(t, testDB.Create(&domain.Tenant{
|
||||
ID: parentID,
|
||||
Name: "Recursive Parent",
|
||||
Slug: "recursive-parent",
|
||||
Type: domain.TenantTypeCompany,
|
||||
Status: domain.TenantStatusActive,
|
||||
}).Error)
|
||||
require.NoError(t, testDB.Create(&domain.Tenant{
|
||||
ID: childID,
|
||||
Name: "Recursive Child",
|
||||
Slug: "recursive-child",
|
||||
Type: domain.TenantTypeOrganization,
|
||||
Status: domain.TenantStatusActive,
|
||||
ParentID: &parentID,
|
||||
}).Error)
|
||||
require.NoError(t, testDB.Create(&domain.Tenant{
|
||||
ID: grandchildID,
|
||||
Name: "Recursive Grandchild",
|
||||
Slug: "recursive-grandchild",
|
||||
Type: domain.TenantTypeUserGroup,
|
||||
Status: domain.TenantStatusActive,
|
||||
ParentID: &childID,
|
||||
}).Error)
|
||||
require.NoError(t, testDB.Create(&domain.Tenant{
|
||||
ID: siblingID,
|
||||
Name: "Recursive Sibling",
|
||||
Slug: "recursive-sibling",
|
||||
Type: domain.TenantTypeCompany,
|
||||
Status: domain.TenantStatusActive,
|
||||
}).Error)
|
||||
|
||||
users := []domain.User{
|
||||
{ID: "21000000-0000-0000-0000-000000000001", Email: "parent@example.com", Name: "Parent", TenantID: &parentID},
|
||||
{ID: "21000000-0000-0000-0000-000000000002", Email: "child@example.com", Name: "Child", TenantID: &childID},
|
||||
{ID: "21000000-0000-0000-0000-000000000003", Email: "grandchild@example.com", Name: "Grandchild", TenantID: &grandchildID},
|
||||
{ID: "21000000-0000-0000-0000-000000000004", Email: "deleted-grandchild@example.com", Name: "Deleted Grandchild", TenantID: &grandchildID},
|
||||
{ID: "21000000-0000-0000-0000-000000000005", Email: "sibling@example.com", Name: "Sibling", TenantID: &siblingID},
|
||||
}
|
||||
for i := range users {
|
||||
require.NoError(t, testDB.Create(&users[i]).Error)
|
||||
}
|
||||
require.NoError(t, testDB.Delete(&domain.User{}, "id = ?", users[3].ID).Error)
|
||||
|
||||
directCounts, err := repo.CountTenantMembers(ctx, []domain.Tenant{{ID: parentID}, {ID: childID}, {ID: grandchildID}, {ID: siblingID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), directCounts[parentID])
|
||||
assert.Equal(t, int64(1), directCounts[childID])
|
||||
assert.Equal(t, int64(1), directCounts[grandchildID])
|
||||
assert.Equal(t, int64(1), directCounts[siblingID])
|
||||
|
||||
recursiveCounts, err := repo.CountTenantMembersRecursive(ctx, []domain.Tenant{{ID: parentID}, {ID: childID}, {ID: grandchildID}, {ID: siblingID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), recursiveCounts[parentID])
|
||||
assert.Equal(t, int64(2), recursiveCounts[childID])
|
||||
assert.Equal(t, int64(1), recursiveCounts[grandchildID])
|
||||
assert.Equal(t, int64(1), recursiveCounts[siblingID])
|
||||
}
|
||||
|
||||
func TestUserProjectionRepository_MarkFailedMakesProjectionNotReady(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := NewUserProjectionRepository(testDB)
|
||||
|
||||
require.NoError(t, testDB.Exec("DELETE FROM user_projection_states").Error)
|
||||
|
||||
require.NoError(t, repo.MarkFailed(ctx, errors.New("kratos down")))
|
||||
|
||||
ready, err := repo.IsReady(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ready)
|
||||
}
|
||||
@@ -272,7 +272,12 @@ func (r *userRepository) List(ctx context.Context, offset, limit int, search str
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.db.WithContext(ctx).Delete(&domain.User{}, "id = ?", id).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&domain.UserLoginID{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Delete(&domain.User{}, "id = ?", id).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestUserRepository(t *testing.T) {
|
||||
@@ -95,8 +96,14 @@ func TestUserRepository(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Delete User", func(t *testing.T) {
|
||||
require.NoError(t, testDB.AutoMigrate(&domain.UserLoginID{}))
|
||||
require.NoError(t, testDB.Exec("DELETE FROM user_login_ids").Error)
|
||||
require.NoError(t, testDB.Exec("DELETE FROM users WHERE email = ?", "delete@example.com").Error)
|
||||
user := &domain.User{Email: "delete@example.com", Name: "To Delete"}
|
||||
_ = repo.Create(ctx, user)
|
||||
require.NoError(t, repo.Create(ctx, user))
|
||||
require.NoError(t, repo.UpdateUserLoginIDs(ctx, user.ID, []domain.UserLoginID{
|
||||
{UserID: user.ID, TenantID: uuid.NewString(), FieldKey: "employee_id", LoginID: "DELETE001"},
|
||||
}))
|
||||
|
||||
err := repo.Delete(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
@@ -104,6 +111,14 @@ func TestUserRepository(t *testing.T) {
|
||||
found, err := repo.FindByEmail(ctx, "delete@example.com")
|
||||
assert.Error(t, err) // Should not be found
|
||||
assert.Nil(t, found)
|
||||
|
||||
var hardDeleted domain.User
|
||||
err = testDB.Unscoped().Where("id = ?", user.ID).First(&hardDeleted).Error
|
||||
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
|
||||
|
||||
var loginIDCount int64
|
||||
require.NoError(t, testDB.Unscoped().Model(&domain.UserLoginID{}).Where("user_id = ?", user.ID).Count(&loginIDCount).Error)
|
||||
require.Zero(t, loginIDCount)
|
||||
})
|
||||
|
||||
t.Run("CountByCompanyCodes", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user