forked from baron/baron-sso
adminfront 조직 통계오류 보정. Kratos Projection용 통계테이블 구조 추가
This commit is contained in:
@@ -6,8 +6,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type UserGroupService interface {
|
||||
@@ -210,40 +212,56 @@ func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string
|
||||
return fmt.Errorf("user group not found: %w", err)
|
||||
}
|
||||
|
||||
// [Fix] Sync Kratos Traits & Local DB when a user is added to an organization
|
||||
if s.kratos != nil && s.tenantRepo != nil {
|
||||
tenant, err := s.tenantRepo.FindByID(ctx, group.TenantID)
|
||||
if err == nil && tenant != nil {
|
||||
// Fetch Kratos Identity
|
||||
identity, err := s.kratos.GetIdentity(ctx, userID)
|
||||
if err == nil && identity != nil {
|
||||
traits := identity.Traits
|
||||
if traits == nil {
|
||||
traits = make(map[string]interface{})
|
||||
}
|
||||
traits["companyCode"] = tenant.Slug
|
||||
traits["tenant_id"] = tenant.ID
|
||||
traits["department"] = group.Name
|
||||
var tenant *domain.Tenant
|
||||
if s.tenantRepo != nil {
|
||||
tenant, _ = s.tenantRepo.FindByID(ctx, group.TenantID)
|
||||
}
|
||||
|
||||
// Update Kratos
|
||||
_, updateErr := s.kratos.UpdateIdentity(ctx, userID, traits, identity.State)
|
||||
if updateErr != nil {
|
||||
slog.Error("Failed to update identity traits during AddMember", "user", userID, "error", updateErr)
|
||||
}
|
||||
var updatedIdentity *KratosIdentity
|
||||
|
||||
// [Fix] Sync Kratos Traits & Local DB when a user is added to an organization
|
||||
if s.kratos != nil && tenant != nil {
|
||||
// Fetch Kratos Identity
|
||||
identity, err := s.kratos.GetIdentity(ctx, userID)
|
||||
if err == nil && identity != nil {
|
||||
traits := identity.Traits
|
||||
if traits == nil {
|
||||
traits = make(map[string]interface{})
|
||||
}
|
||||
traits["companyCode"] = tenant.Slug
|
||||
traits["tenant_id"] = tenant.ID
|
||||
traits["department"] = group.Name
|
||||
|
||||
// Update Kratos
|
||||
updated, updateErr := s.kratos.UpdateIdentity(ctx, userID, traits, identity.State)
|
||||
if updateErr != nil {
|
||||
slog.Error("Failed to update identity traits during AddMember", "user", userID, "error", updateErr)
|
||||
} else if updated != nil {
|
||||
updatedIdentity = updated
|
||||
} else {
|
||||
identity.Traits = traits
|
||||
updatedIdentity = identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync local user repo
|
||||
if s.userRepo != nil && s.tenantRepo != nil {
|
||||
tenant, _ := s.tenantRepo.FindByID(ctx, group.TenantID)
|
||||
if tenant != nil {
|
||||
localUser, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err == nil && localUser != nil {
|
||||
localUser.CompanyCode = tenant.Slug
|
||||
localUser.TenantID = &tenant.ID
|
||||
localUser.Department = group.Name
|
||||
_ = s.userRepo.Update(ctx, localUser)
|
||||
if s.userRepo != nil && tenant != nil {
|
||||
localUser, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil || localUser == nil {
|
||||
if updatedIdentity != nil {
|
||||
localUser = mapUserGroupKratosIdentityToLocalUser(*updatedIdentity)
|
||||
} else {
|
||||
slog.Warn("Skipping local user sync during AddMember because identity projection is unavailable", "user", userID, "error", err)
|
||||
localUser = nil
|
||||
}
|
||||
}
|
||||
if localUser != nil {
|
||||
localUser.CompanyCode = tenant.Slug
|
||||
localUser.TenantID = &tenant.ID
|
||||
localUser.Department = group.Name
|
||||
if err := s.userRepo.Update(ctx, localUser); err != nil {
|
||||
slog.Error("Failed to sync local user during AddMember", "user", userID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +289,116 @@ func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapUserGroupKratosIdentityToLocalUser(identity KratosIdentity) *domain.User {
|
||||
traits := identity.Traits
|
||||
now := time.Now()
|
||||
createdAt := identity.CreatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = now
|
||||
}
|
||||
updatedAt := identity.UpdatedAt
|
||||
if updatedAt.IsZero() {
|
||||
updatedAt = now
|
||||
}
|
||||
|
||||
role := userGroupTraitString(traits, "grade")
|
||||
if role == "" {
|
||||
role = userGroupTraitString(traits, "role")
|
||||
}
|
||||
role = domain.NormalizeRole(role)
|
||||
if role == "" {
|
||||
role = domain.RoleUser
|
||||
}
|
||||
|
||||
companyCode := userGroupTraitString(traits, "companyCode")
|
||||
if companyCode == "" {
|
||||
companyCode = userGroupTraitString(traits, "company_code")
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
ID: identity.ID,
|
||||
Email: userGroupTraitString(traits, "email"),
|
||||
Name: userGroupTraitString(traits, "name"),
|
||||
Phone: userGroupTraitString(traits, "phone_number"),
|
||||
Role: role,
|
||||
Status: userGroupIdentityStatus(identity.State),
|
||||
CompanyCode: companyCode,
|
||||
Department: userGroupTraitString(traits, "department"),
|
||||
Position: userGroupTraitString(traits, "position"),
|
||||
JobTitle: userGroupTraitString(traits, "jobTitle"),
|
||||
AffiliationType: userGroupTraitString(traits, "affiliationType"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
Metadata: make(domain.JSONMap),
|
||||
}
|
||||
if tenantID := userGroupTraitString(traits, "tenant_id"); tenantID != "" {
|
||||
user.TenantID = &tenantID
|
||||
}
|
||||
if relyingPartyID := userGroupTraitString(traits, "relying_party_id"); relyingPartyID != "" {
|
||||
user.RelyingPartyID = &relyingPartyID
|
||||
}
|
||||
user.CompanyCodes = pq.StringArray(userGroupTraitStringArray(traits, "companyCodes"))
|
||||
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
"grade": true, "role": true, "companyCode": true, "company_code": true,
|
||||
"companyCodes": true, "tenant_id": true, "department": true,
|
||||
"position": true, "jobTitle": true, "affiliationType": true,
|
||||
"relying_party_id": true, "custom_login_ids": true, "id": true,
|
||||
}
|
||||
for key, value := range traits {
|
||||
if !coreTraits[key] {
|
||||
user.Metadata[key] = value
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func userGroupTraitString(traits map[string]interface{}, key string) string {
|
||||
if traits == nil {
|
||||
return ""
|
||||
}
|
||||
value, ok := traits[key]
|
||||
if !ok || value == nil {
|
||||
return ""
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
|
||||
func userGroupTraitStringArray(traits map[string]interface{}, key string) []string {
|
||||
if traits == nil {
|
||||
return nil
|
||||
}
|
||||
switch value := traits[key].(type) {
|
||||
case []string:
|
||||
return value
|
||||
case []interface{}:
|
||||
items := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
if str, ok := item.(string); ok && str != "" {
|
||||
items = append(items, str)
|
||||
}
|
||||
}
|
||||
return items
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func userGroupIdentityStatus(state string) string {
|
||||
switch state {
|
||||
case "", "active":
|
||||
return domain.UserStatusActive
|
||||
case "inactive":
|
||||
return domain.UserStatusInactive
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
||||
// Validate group exists
|
||||
if _, err := s.repo.FindByID(ctx, groupID); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Mocks for Repositories ---
|
||||
@@ -45,10 +46,15 @@ func (m *MockUserGroupRepository) ListByTenantID(ctx context.Context, tenantID s
|
||||
|
||||
type MockUserRepository struct {
|
||||
mock.Mock
|
||||
updatedUsers []domain.User
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Create(ctx context.Context, user *domain.User) error { return nil }
|
||||
func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) error { return nil }
|
||||
func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||
copied := *user
|
||||
m.updatedUsers = append(m.updatedUsers, copied)
|
||||
return nil
|
||||
}
|
||||
func (m *MockUserRepository) Delete(ctx context.Context, id string) error {
|
||||
return m.Called(ctx, id).Error(0)
|
||||
}
|
||||
@@ -270,6 +276,62 @@ func TestUserGroupService_AddMember(t *testing.T) {
|
||||
// mockUserRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserGroupService_AddMemberUpsertsLocalReadModelWhenMissing(t *testing.T) {
|
||||
mockOutbox := new(MockKetoOutboxRepositoryShared)
|
||||
mockUserGroupRepo := new(MockUserGroupRepository)
|
||||
mockUserRepo := new(MockUserRepository)
|
||||
mockTenantRepo := new(MockTenantRepository)
|
||||
mockKratos := new(MockKratosAdminServiceShared)
|
||||
svc := NewUserGroupService(mockUserGroupRepo, mockUserRepo, mockTenantRepo, nil, mockOutbox, mockKratos)
|
||||
|
||||
groupID := "group-1"
|
||||
userID := "user-1"
|
||||
tenantID := "tenant-1"
|
||||
tenantSlug := "tenant-slug"
|
||||
|
||||
mockUserGroupRepo.On("FindByID", mock.Anything, groupID).Return(&domain.UserGroup{ID: groupID, TenantID: tenantID, Name: "Sales"}, nil)
|
||||
mockUserRepo.On("FindByID", mock.Anything, userID).Return(nil, gorm.ErrRecordNotFound)
|
||||
mockTenantRepo.On("FindByID", mock.Anything, tenantID).Return(&domain.Tenant{ID: tenantID, Slug: tenantSlug}, nil)
|
||||
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&KratosIdentity{
|
||||
ID: userID,
|
||||
Traits: map[string]interface{}{
|
||||
"email": "user@test.com",
|
||||
"name": "User Test",
|
||||
},
|
||||
State: "active",
|
||||
}, nil)
|
||||
mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool {
|
||||
return traits["companyCode"] == tenantSlug && traits["tenant_id"] == tenantID && traits["department"] == "Sales"
|
||||
}), "active").Return(&KratosIdentity{
|
||||
ID: userID,
|
||||
Traits: map[string]interface{}{
|
||||
"email": "user@test.com",
|
||||
"name": "User Test",
|
||||
"companyCode": tenantSlug,
|
||||
"tenant_id": tenantID,
|
||||
"department": "Sales",
|
||||
},
|
||||
State: "active",
|
||||
}, nil)
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
|
||||
return e.Namespace == "Tenant" && e.Object == groupID && e.Relation == "members" && e.Subject == "User:"+userID
|
||||
})).Return(nil).Once()
|
||||
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
|
||||
return e.Namespace == "Tenant" && e.Object == tenantID && e.Relation == "members" && e.Subject == "User:"+userID
|
||||
})).Return(nil).Once()
|
||||
|
||||
err := svc.AddMember(context.Background(), groupID, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, mockUserRepo.updatedUsers, 1)
|
||||
assert.Equal(t, userID, mockUserRepo.updatedUsers[0].ID)
|
||||
assert.Equal(t, tenantSlug, mockUserRepo.updatedUsers[0].CompanyCode)
|
||||
assert.NotNil(t, mockUserRepo.updatedUsers[0].TenantID)
|
||||
assert.Equal(t, tenantID, *mockUserRepo.updatedUsers[0].TenantID)
|
||||
assert.Equal(t, "Sales", mockUserRepo.updatedUsers[0].Department)
|
||||
mockOutbox.AssertExpectations(t)
|
||||
mockKratos.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserGroupService_AssignRoleToTenant(t *testing.T) {
|
||||
mockOutbox := new(MockKetoOutboxRepositoryShared)
|
||||
mockUserGroupRepo := new(MockUserGroupRepository)
|
||||
|
||||
163
backend/internal/service/user_projection_sync_service.go
Normal file
163
backend/internal/service/user_projection_sync_service.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type UserProjectionSyncService struct {
|
||||
kratos KratosAdminService
|
||||
repo repository.UserProjectionRepository
|
||||
}
|
||||
|
||||
type UserProjectionReconciler interface {
|
||||
Reconcile(ctx context.Context) (int, error)
|
||||
}
|
||||
|
||||
func NewUserProjectionSyncService(kratos KratosAdminService, repo repository.UserProjectionRepository) *UserProjectionSyncService {
|
||||
return &UserProjectionSyncService{
|
||||
kratos: kratos,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserProjectionSyncService) Reconcile(ctx context.Context) (int, error) {
|
||||
if s == nil || s.kratos == nil || s.repo == nil {
|
||||
return 0, fmt.Errorf("user projection sync dependencies are not configured")
|
||||
}
|
||||
|
||||
identities, err := s.kratos.ListIdentities(ctx)
|
||||
if err != nil {
|
||||
_ = s.repo.MarkFailed(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
users := make([]domain.User, 0, len(identities))
|
||||
for _, identity := range identities {
|
||||
users = append(users, MapKratosIdentityToLocalUser(identity))
|
||||
}
|
||||
if err := s.repo.ReplaceAllFromKratos(ctx, users); err != nil {
|
||||
_ = s.repo.MarkFailed(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
return len(users), nil
|
||||
}
|
||||
|
||||
func MapKratosIdentityToLocalUser(identity KratosIdentity) domain.User {
|
||||
traits := identity.Traits
|
||||
now := time.Now()
|
||||
createdAt := identity.CreatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = now
|
||||
}
|
||||
updatedAt := identity.UpdatedAt
|
||||
if updatedAt.IsZero() {
|
||||
updatedAt = now
|
||||
}
|
||||
|
||||
role := kratosProjectionTraitString(traits, "grade")
|
||||
if role == "" {
|
||||
role = kratosProjectionTraitString(traits, "role")
|
||||
}
|
||||
role = domain.NormalizeRole(role)
|
||||
if role == "" {
|
||||
role = domain.RoleUser
|
||||
}
|
||||
|
||||
companyCode := kratosProjectionTraitString(traits, "companyCode")
|
||||
if companyCode == "" {
|
||||
companyCode = kratosProjectionTraitString(traits, "company_code")
|
||||
}
|
||||
|
||||
user := domain.User{
|
||||
ID: identity.ID,
|
||||
Email: kratosProjectionTraitString(traits, "email"),
|
||||
Name: kratosProjectionTraitString(traits, "name"),
|
||||
Phone: kratosProjectionTraitString(traits, "phone_number"),
|
||||
Role: role,
|
||||
Status: normalizeProjectionStatus(identity.State),
|
||||
CompanyCode: companyCode,
|
||||
CompanyCodes: pq.StringArray(kratosProjectionTraitStringArray(traits, "companyCodes")),
|
||||
Department: kratosProjectionTraitString(traits, "department"),
|
||||
Position: kratosProjectionTraitString(traits, "position"),
|
||||
JobTitle: kratosProjectionTraitString(traits, "jobTitle"),
|
||||
AffiliationType: kratosProjectionTraitString(traits, "affiliationType"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
Metadata: make(domain.JSONMap),
|
||||
}
|
||||
if tenantID := kratosProjectionTraitString(traits, "tenant_id"); tenantID != "" {
|
||||
user.TenantID = &tenantID
|
||||
}
|
||||
if relyingPartyID := kratosProjectionTraitString(traits, "relying_party_id"); relyingPartyID != "" {
|
||||
user.RelyingPartyID = &relyingPartyID
|
||||
}
|
||||
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
"grade": true, "role": true,
|
||||
"companyCode": true, "company_code": true, "companyCodes": true,
|
||||
"tenant_id": true, "department": true,
|
||||
"position": true, "jobTitle": true, "affiliationType": true,
|
||||
"relying_party_id": true, "custom_login_ids": true, "id": true,
|
||||
}
|
||||
for key, value := range traits {
|
||||
if !coreTraits[key] {
|
||||
user.Metadata[key] = value
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func kratosProjectionTraitString(traits map[string]interface{}, key string) string {
|
||||
if traits == nil {
|
||||
return ""
|
||||
}
|
||||
value, ok := traits[key]
|
||||
if !ok || value == nil {
|
||||
return ""
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
|
||||
func kratosProjectionTraitStringArray(traits map[string]interface{}, key string) []string {
|
||||
if traits == nil {
|
||||
return nil
|
||||
}
|
||||
switch value := traits[key].(type) {
|
||||
case []string:
|
||||
return value
|
||||
case []interface{}:
|
||||
items := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
if str, ok := item.(string); ok && strings.TrimSpace(str) != "" {
|
||||
items = append(items, str)
|
||||
}
|
||||
}
|
||||
return items
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeProjectionStatus(state string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "blocked", domain.UserStatusInactive:
|
||||
return domain.UserStatusInactive
|
||||
case domain.UserStatusSuspended:
|
||||
return domain.UserStatusSuspended
|
||||
case domain.UserStatusLeaveOfAbsence:
|
||||
return domain.UserStatusLeaveOfAbsence
|
||||
default:
|
||||
return domain.UserStatusActive
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeUserProjectionRepo struct {
|
||||
replacedUsers []domain.User
|
||||
failedErr error
|
||||
replaceErr error
|
||||
}
|
||||
|
||||
func (f *fakeUserProjectionRepo) IsReady(ctx context.Context) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProjectionRepo) GetStatus(ctx context.Context) (domain.UserProjectionStatus, error) {
|
||||
return domain.UserProjectionStatus{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProjectionRepo) CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserProjectionRepo) ReplaceAllFromKratos(ctx context.Context, users []domain.User) error {
|
||||
f.replacedUsers = append([]domain.User(nil), users...)
|
||||
return f.replaceErr
|
||||
}
|
||||
|
||||
func (f *fakeUserProjectionRepo) MarkFailed(ctx context.Context, syncErr error) error {
|
||||
f.failedErr = syncErr
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUserProjectionSyncService_ReconcileReplacesProjectionFromKratos(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
kratos := new(MockKratosAdminServiceShared)
|
||||
repo := &fakeUserProjectionRepo{}
|
||||
svc := NewUserProjectionSyncService(kratos, repo)
|
||||
|
||||
tenantID := "00000000-0000-0000-0000-000000000001"
|
||||
kratos.On("ListIdentities", ctx).Return([]KratosIdentity{
|
||||
{
|
||||
ID: "00000000-0000-0000-0000-000000000101",
|
||||
Traits: map[string]interface{}{
|
||||
"email": "one@example.com",
|
||||
"name": "One",
|
||||
"phone_number": "+821012345678",
|
||||
"companyCode": "saman",
|
||||
"companyCodes": []interface{}{"saman", "group-a"},
|
||||
"tenant_id": tenantID,
|
||||
"department": "DX",
|
||||
"customAttr": "kept",
|
||||
},
|
||||
State: "active",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
count, err := svc.Reconcile(ctx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
require.Len(t, repo.replacedUsers, 1)
|
||||
assert.Equal(t, "one@example.com", repo.replacedUsers[0].Email)
|
||||
assert.Equal(t, "One", repo.replacedUsers[0].Name)
|
||||
assert.Equal(t, "+821012345678", repo.replacedUsers[0].Phone)
|
||||
assert.Equal(t, "saman", repo.replacedUsers[0].CompanyCode)
|
||||
assert.Equal(t, []string{"saman", "group-a"}, []string(repo.replacedUsers[0].CompanyCodes))
|
||||
require.NotNil(t, repo.replacedUsers[0].TenantID)
|
||||
assert.Equal(t, tenantID, *repo.replacedUsers[0].TenantID)
|
||||
assert.Equal(t, "kept", repo.replacedUsers[0].Metadata["customAttr"])
|
||||
assert.NoError(t, repo.failedErr)
|
||||
kratos.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserProjectionSyncService_ReconcileMarksFailedWhenKratosFails(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
kratos := new(MockKratosAdminServiceShared)
|
||||
repo := &fakeUserProjectionRepo{}
|
||||
svc := NewUserProjectionSyncService(kratos, repo)
|
||||
|
||||
expectedErr := errors.New("kratos down")
|
||||
kratos.On("ListIdentities", ctx).Return([]KratosIdentity{}, expectedErr).Once()
|
||||
|
||||
count, err := svc.Reconcile(ctx)
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
assert.ErrorIs(t, repo.failedErr, expectedErr)
|
||||
assert.Empty(t, repo.replacedUsers)
|
||||
kratos.AssertExpectations(t)
|
||||
}
|
||||
Reference in New Issue
Block a user