package service import ( "baron-sso-backend/internal/domain" "context" "testing" "github.com/stretchr/testify/require" ) func TestWorksmobileSyncServiceRejectsAliasLocalPartAlreadyUsedByOtherUser(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") rootID := "root-tenant" tenantID := "saman-tenant" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } tenant := domain.Tenant{ ID: tenantID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}, } target := domain.User{ ID: "target-user", Email: "target@samaneng.com", Name: "Target", TenantID: &tenantID, Metadata: domain.JSONMap{ "aliasEmails": []any{"used@hanmaceng.co.kr"}, }, } existing := domain.User{ ID: "existing-user", Email: "used@samaneng.com", Name: "Existing", TenantID: &tenantID, } outboxRepo := &fakeWorksmobileOutboxRepo{} service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}}, &fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target, existing}}, outboxRepo, nil, ) item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID) require.Nil(t, item) require.Error(t, err) require.Contains(t, err.Error(), "이미 사용 중") require.Empty(t, outboxRepo.created) } func TestWorksmobileSyncServiceOverviewExposesAdminTenantIDForPasswordManageLink(t *testing.T) { t.Setenv("WORKS_ADMIN_TENANT_ID", "works-tenant-1") root := domain.Tenant{ ID: "root-tenant", Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{root.ID: root}}, &fakeWorksmobileUserRepo{}, &fakeWorksmobileOutboxRepo{}, nil, ) overview, err := service.GetTenantOverview(context.Background(), root.ID) require.NoError(t, err) require.Equal(t, "works-tenant-1", overview.Config.AdminTenantID) } func TestCompareWorksmobileGroupsUsesOrganizationsAndBarongroupChildCompanies(t *testing.T) { parentID := "root-tenant" root := domain.Tenant{ ID: parentID, Name: "한맥가족", Slug: HanmacFamilyTenantSlug, Type: domain.TenantTypeCompanyGroup, } hanmac := domain.Tenant{ ID: "hanmac-tenant", Name: "한맥기술", Slug: "hanmac", Type: domain.TenantTypeCompany, ParentID: &parentID, } barongroup := domain.Tenant{ ID: "barongroup-tenant", Name: "바론그룹", Slug: "baron-group", Type: domain.TenantTypeCompany, ParentID: &parentID, } barongroupChildCompany := domain.Tenant{ ID: "barongroup-child-company", Name: "바론그룹 하위 회사", Type: domain.TenantTypeCompany, ParentID: &barongroup.ID, } organization := domain.Tenant{ ID: "organization-tenant", Name: "정규 조직", Type: domain.TenantTypeOrganization, ParentID: &hanmac.ID, } legacyUserGroup := domain.Tenant{ ID: "legacy-user-group-tenant", Name: "레거시 사용자 그룹", Type: domain.TenantTypeUserGroup, ParentID: &hanmac.ID, } items := compareWorksmobileGroups( []domain.Tenant{root, hanmac, barongroup, barongroupChildCompany, organization, legacyUserGroup}, []WorksmobileRemoteGroup{ {ID: "works-root", ExternalID: root.ID, DisplayName: root.Name}, {ID: "works-hanmac", ExternalID: hanmac.ID, DisplayName: hanmac.Name}, {ID: "works-barongroup", ExternalID: barongroup.ID, DisplayName: barongroup.Name}, {ID: "works-barongroup-child", ExternalID: barongroupChildCompany.ID, DisplayName: barongroupChildCompany.Name}, {ID: "works-organization", ExternalID: organization.ID, DisplayName: organization.Name}, {ID: "works-legacy-user-group", ExternalID: legacyUserGroup.ID, DisplayName: legacyUserGroup.Name}, {ID: "works-orphan", ExternalID: "works-orphan", DisplayName: "WORKS 전용 조직"}, }, true, ) require.Len(t, items, 3) require.Equal(t, barongroupChildCompany.ID, items[0].BaronID) require.Equal(t, "matched", items[0].Status) require.Equal(t, organization.ID, items[1].BaronID) require.Equal(t, "matched", items[1].Status) require.Equal(t, "works-orphan", items[2].ExternalKey) require.Equal(t, "missing_in_baron", items[2].Status) } func TestWorksmobileSyncServiceRejectsDomainCompanyOrgUnitSync(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") rootID := "root-tenant" companyID := "company-tenant" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } company := domain.Tenant{ ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}, } outboxRepo := &fakeWorksmobileOutboxRepo{} service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}}, &fakeWorksmobileUserRepo{}, outboxRepo, nil, ) item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, companyID) require.Nil(t, item) require.Error(t, err) require.Contains(t, err.Error(), "worksmobile orgunit tenant") require.Empty(t, outboxRepo.created) } func TestWorksmobileSyncServiceEnqueuesBarongroupChildCompanyOrgUnitSync(t *testing.T) { t.Setenv("BARONGROUP_DOMAIN_ID", "1004") rootID := "root-tenant" barongroupID := "barongroup-tenant" companyID := "barongroup-child-company" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } barongroup := domain.Tenant{ ID: barongroupID, Slug: "baron-group", Name: "바론그룹", Type: domain.TenantTypeCompany, ParentID: &rootID, } company := domain.Tenant{ ID: companyID, Slug: "barongroup-child", Name: "바론그룹 하위 회사", Type: domain.TenantTypeCompany, ParentID: &barongroupID, } outboxRepo := &fakeWorksmobileOutboxRepo{} service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, barongroupID: barongroup, companyID: company}, list: []domain.Tenant{root, barongroup, company}}, &fakeWorksmobileUserRepo{}, outboxRepo, nil, ) item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, companyID) require.NoError(t, err) require.NotNil(t, item) require.Len(t, outboxRepo.created, 1) request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload) require.Equal(t, companyID, request.OrgUnitExternalKey) require.Empty(t, request.ParentOrgUnitID) } func TestWorksmobileSyncServiceEnqueuesOrganizationOrgUnitSync(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") rootID := "root-tenant" companyID := "company-tenant" organizationID := "organization-tenant" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } company := domain.Tenant{ ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}, } organization := domain.Tenant{ ID: organizationID, Slug: "engineering", Name: "기술본부", Type: domain.TenantTypeOrganization, ParentID: &companyID, } outboxRepo := &fakeWorksmobileOutboxRepo{} service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{ tenants: map[string]domain.Tenant{rootID: root, companyID: company, organizationID: organization}, list: []domain.Tenant{root, company, organization}, }, &fakeWorksmobileUserRepo{}, outboxRepo, nil, ) item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, organizationID) require.NoError(t, err) require.NotNil(t, item) require.Len(t, outboxRepo.created, 1) request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload) require.Equal(t, organizationID, request.OrgUnitExternalKey) require.Empty(t, request.ParentOrgUnitID) } func TestWorksmobileDomainClassificationUsesAncestorCompanyForGPDTDCOrganization(t *testing.T) { t.Setenv("GPDTDC_DOMAIN_ID", "1003") rootID := "root-tenant" companyID := "company-tenant" organizationID := "organization-tenant" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } company := domain.Tenant{ ID: companyID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "baroncs.co.kr"}}, } organization := domain.Tenant{ ID: organizationID, Slug: "gpd", Name: "총괄기획실", Type: domain.TenantTypeOrganization, ParentID: &companyID, } tenantByID := worksmobileTenantByID([]domain.Tenant{root, company, organization}) domainTenant := worksmobileDomainClassificationTenant(organization, tenantByID) payload, err := BuildWorksmobileOrgUnitPayloadForDomainTenant(organization, domainTenant, nil, 1) require.NoError(t, err) require.Equal(t, companyID, domainTenant.ID) require.Equal(t, int64(1003), payload.DomainID) require.Equal(t, "gpd@baroncs.co.kr", payload.Email) } func TestWorksmobileSyncServiceKeepsCompanyUsersInComparisonScope(t *testing.T) { rootID := "root-tenant" companyID := "company-tenant" userGroupID := "user-group-tenant" root := domain.Tenant{ ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", } company := domain.Tenant{ ID: companyID, Name: "계열사", Type: domain.TenantTypeCompany, ParentID: &rootID, } userGroup := domain.Tenant{ ID: userGroupID, Name: "연동 조직", Type: domain.TenantTypeOrganization, ParentID: &companyID, } userRepo := &fakeWorksmobileUserRepo{} service := NewWorksmobileSyncService( &fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company, userGroupID: userGroup}, list: []domain.Tenant{root, company, userGroup}}, userRepo, &fakeWorksmobileOutboxRepo{}, &fakeWorksmobileDirectoryClient{}, ) _, err := service.GetComparison(context.Background(), rootID, true) require.NoError(t, err) require.ElementsMatch(t, []string{companyID, userGroupID}, userRepo.requestedTenantIDs) } type fakeWorksmobileTenantService struct { tenants map[string]domain.Tenant list []domain.Tenant } func (f *fakeWorksmobileTenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) { tenant := f.tenants[id] return &tenant, nil } func (f *fakeWorksmobileTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) { return f.list, int64(len(f.list)), nil } func (f *fakeWorksmobileTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) { return false, nil } func (f *fakeWorksmobileTenantService) ApproveTenant(ctx context.Context, id string) error { return nil } func (f *fakeWorksmobileTenantService) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) { return nil, nil } func (f *fakeWorksmobileTenantService) SetKetoService(keto KetoService) {} func (f *fakeWorksmobileTenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error { return nil } type fakeWorksmobileUserRepo struct { byID map[string]domain.User byTenant []domain.User requestedTenantIDs []string } func (f *fakeWorksmobileUserRepo) Create(ctx context.Context, user *domain.User) error { return nil } func (f *fakeWorksmobileUserRepo) Update(ctx context.Context, user *domain.User) error { return nil } func (f *fakeWorksmobileUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) { user := f.byID[id] return &user, nil } func (f *fakeWorksmobileUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, companyCode string) ([]domain.User, int64, error) { return nil, 0, nil } func (f *fakeWorksmobileUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) { return 0, nil } func (f *fakeWorksmobileUserRepo) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) { f.requestedTenantIDs = append([]string(nil), tenantIDs...) return f.byTenant, nil } func (f *fakeWorksmobileUserRepo) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) Delete(ctx context.Context, id string) error { return nil } func (f *fakeWorksmobileUserRepo) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error { return nil } func (f *fakeWorksmobileUserRepo) GetUserLoginIDs(ctx context.Context, userID string) ([]domain.UserLoginID, error) { return nil, nil } func (f *fakeWorksmobileUserRepo) IsLoginIDTaken(ctx context.Context, loginID string) (bool, error) { return false, nil } func (f *fakeWorksmobileUserRepo) FindTenantIDByLoginID(ctx context.Context, loginID string) (string, error) { return "", nil }