1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/worksmobile_sync_service_test.go

1780 lines
58 KiB
Go

package service
import (
"baron-sso-backend/internal/domain"
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestWorksmobileSyncServiceRejectsAliasEmailAlreadyUsedByOtherUser(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@hanmaceng.co.kr",
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 TestWorksmobileSyncServiceEnqueuesSuspendedUserStatusWithOrganizations(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusSuspended,
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}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionSuspend, outboxRepo.created[0].Action)
require.Equal(t, domain.UserStatusSuspended, outboxRepo.created[0].Payload["baronStatus"])
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileUserPayload)
require.True(t, ok)
require.NotEmpty(t, request.Organizations)
require.Equal(t, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
}
func TestWorksmobileSyncServiceEnqueuesUserCredentialBatchID(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
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}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "batch-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, "batch-1", outboxRepo.created[0].Payload["credentialBatchId"])
require.NotEmpty(t, outboxRepo.created[0].Payload["credentialBatchCreatedAt"])
require.Equal(t, "Target", outboxRepo.created[0].Payload["displayName"])
require.Equal(t, "Saman", outboxRepo.created[0].Payload["primaryLeafOrgName"])
}
func TestWorksmobileSyncServiceEnqueuesUserPasswordResetCredentialBatch(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-leaf"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "people-growth",
Name: "인재성장",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
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}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserPasswordReset(context.Background(), rootID, target.ID, "reset-batch-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionPasswordReset, outboxRepo.created[0].Action)
require.Equal(t, "reset-batch-1", outboxRepo.created[0].Payload["credentialBatchId"])
require.Equal(t, "worksmobile_password_reset", outboxRepo.created[0].Payload["credentialOperation"])
require.NotEmpty(t, outboxRepo.created[0].Payload["credentialBatchCreatedAt"])
require.Equal(t, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
require.Equal(t, "Target", outboxRepo.created[0].Payload["displayName"])
require.Equal(t, "인재성장", outboxRepo.created[0].Payload["primaryLeafOrgName"])
require.NotEmpty(t, outboxRepo.created[0].Payload["initialPassword"])
}
func TestWorksmobileSyncServiceFiltersInitialPasswordsByCredentialBatchID(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
outboxRepo := &fakeWorksmobileOutboxRepo{
credentialBatchJobs: []domain.WorksmobileOutbox{
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "batch-user@samaneng.com",
"displayName": "Batch User",
"primaryLeafOrgName": "인재성장",
"initialPassword": "BatchPass1!",
"credentialBatchId": "batch-1",
},
},
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "other-user@samaneng.com",
"initialPassword": "OtherPass1!",
"credentialBatchId": "batch-2",
},
},
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "legacy-user@samaneng.com",
"initialPassword": "LegacyPass1!",
},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
credentials, err := service.ListInitialPasswordCredentials(context.Background(), rootID, "batch-1")
require.NoError(t, err)
require.Equal(t, []WorksmobileInitialPasswordCredential{
{
Email: "batch-user@samaneng.com",
Name: "Batch User",
PrimaryLeafOrgName: "인재성장",
InitialPassword: "BatchPass1!",
Status: domain.WorksmobileOutboxStatusProcessed,
},
}, credentials)
}
func TestWorksmobileSyncServiceDeletesCredentialBatchPasswordsButKeepsHistory(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
outboxRepo := &fakeWorksmobileOutboxRepo{
credentialBatchJobs: []domain.WorksmobileOutbox{
{
ID: "job-1",
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "batch-user@samaneng.com",
"initialPassword": "BatchPass1!",
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"request": map[string]any{"passwordConfig": map[string]any{"password": "BatchPass1!"}},
},
},
{
ID: "job-2",
ResourceID: "failed-user",
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusFailed,
RetryCount: 2,
LastError: "worksmobile api failed",
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "failed-user@samaneng.com",
"initialPassword": "FailedPass1!",
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"request": map[string]any{"passwordConfig": map[string]any{"password": "FailedPass1!"}},
},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
before, err := service.ListCredentialBatches(context.Background(), rootID)
require.NoError(t, err)
require.Len(t, before, 1)
require.True(t, before[0].HasPasswords)
require.Equal(t, 1, before[0].FailedCount)
require.Len(t, before[0].Failures, 1)
require.Equal(t, "failed-user", before[0].Failures[0].UserID)
require.Equal(t, "failed-user@samaneng.com", before[0].Failures[0].Email)
require.Equal(t, "worksmobile api failed", before[0].Failures[0].LastError)
after, err := service.DeleteCredentialBatchPasswords(context.Background(), rootID, "batch-1")
require.NoError(t, err)
require.Equal(t, "batch-1", after.BatchID)
require.False(t, after.HasPasswords)
require.Equal(t, 2, after.UserCount)
require.NotEmpty(t, after.DeletedAt)
require.Len(t, outboxRepo.payloadUpdates, 2)
require.Empty(t, stringValue(outboxRepo.payloadUpdates[0]["initialPassword"]))
require.Empty(t, stringValue(outboxRepo.payloadUpdates[1]["initialPassword"]))
request := outboxRepo.payloadUpdates[0]["request"].(map[string]any)
passwordConfig := request["passwordConfig"].(map[string]any)
require.Empty(t, stringValue(passwordConfig["password"]))
}
func TestAggregateWorksmobileCredentialBatchesUsesCredentialBatchCreatedAt(t *testing.T) {
oldCreatedAt := time.Date(2026, 5, 29, 1, 4, 15, 0, time.UTC)
batchCreatedAt := time.Date(2026, 6, 1, 7, 20, 0, 0, time.UTC)
batches := aggregateWorksmobileCredentialBatches([]domain.WorksmobileOutbox{
{
ID: "job-1",
CreatedAt: oldCreatedAt,
UpdatedAt: batchCreatedAt.Add(time.Minute),
Status: domain.WorksmobileOutboxStatusPending,
Payload: domain.JSONMap{
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"credentialBatchCreatedAt": batchCreatedAt.Format(time.RFC3339),
},
},
})
require.Len(t, batches, 1)
require.Equal(t, batchCreatedAt, batches[0].CreatedAt)
}
func TestWorksmobileSyncServiceDeprovisionsArchivedUser(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "archived-user",
Email: "archived@samaneng.com",
Name: "Archived",
Status: domain.UserStatusArchived,
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}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "")
require.NoError(t, err)
require.NotNil(t, item)
require.Equal(t, domain.WorksmobileActionDelete, item.Action)
require.Len(t, outboxRepo.created, 1)
err = service.EnqueueUserUpsertIfInScope(context.Background(), target)
require.NoError(t, err)
require.Len(t, outboxRepo.created, 2)
require.Equal(t, domain.WorksmobileActionDelete, outboxRepo.created[1].Action)
}
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 TestWorksmobileSyncServiceOverviewKeepsSafeRecentJobChangeLogPayload(t *testing.T) {
root := domain.Tenant{
ID: "root-tenant",
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
outboxRepo := &fakeWorksmobileOutboxRepo{
recent: []domain.WorksmobileOutbox{
{
ID: "job-user-upsert",
ResourceType: domain.WorksmobileResourceUser,
ResourceID: "user-1",
Action: domain.WorksmobileActionUpsert,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"loginEmail": "changed@example.com",
"displayName": "변경 사용자",
"primaryLeafOrgName": "인재성장",
"initialPassword": "Secret123!",
"request": WorksmobileUserPayload{
Email: "changed@example.com",
UserExternalKey: "user-1",
UserName: WorksmobileUserName{LastName: "변경 사용자"},
PasswordConfig: WorksmobilePasswordConfig{Password: "Secret123!"},
},
},
},
{
ID: "job-org-upsert",
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: "org-1",
Action: domain.WorksmobileActionUpsert,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"matchLocalPart": "people-growth",
"request": WorksmobileOrgUnitPayload{
OrgUnitName: "인재성장",
Email: "people-growth@example.com",
OrgUnitExternalKey: "org-1",
ParentOrgUnitID: "externalKey:parent-1",
},
},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{root.ID: root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
overview, err := service.GetTenantOverview(context.Background(), root.ID)
require.NoError(t, err)
require.Len(t, overview.RecentJobs, 2)
userPayload := overview.RecentJobs[0].Payload
require.Equal(t, "changed@example.com", userPayload["loginEmail"])
require.Equal(t, "변경 사용자", userPayload["displayName"])
require.Equal(t, "인재성장", userPayload["primaryLeafOrgName"])
require.NotContains(t, userPayload, "initialPassword")
require.NotContains(t, userPayload, "request")
require.Equal(t, domain.JSONMap{
"email": "changed@example.com",
"displayName": "변경 사용자",
"userExternalKey": "user-1",
}, userPayload["requestSummary"])
orgPayload := overview.RecentJobs[1].Payload
require.Equal(t, "people-growth", orgPayload["matchLocalPart"])
require.NotContains(t, orgPayload, "request")
require.Equal(t, domain.JSONMap{
"email": "people-growth@example.com",
"orgUnitName": "인재성장",
"orgUnitExternalKey": "org-1",
"parentOrgUnitId": "externalKey:parent-1",
}, orgPayload["requestSummary"])
}
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,
}
userGroup := domain.Tenant{
ID: "legacy-user-group-tenant",
Name: "사용자 그룹 조직",
Type: domain.TenantTypeUserGroup,
ParentID: &hanmac.ID,
}
items := compareWorksmobileGroups(
[]domain.Tenant{root, hanmac, barongroup, barongroupChildCompany, organization, userGroup},
[]WorksmobileRemoteGroup{
{ID: "works-root", ExternalID: root.ID, DisplayName: root.Name},
{ID: "works-hanmac", ExternalID: hanmac.ID, DisplayName: hanmac.Name, Email: "hanmac@hanmaceng.co.kr"},
{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, ParentID: "works-hanmac"},
{ID: "works-user-group", ExternalID: userGroup.ID, DisplayName: userGroup.Name, ParentID: "works-hanmac"},
{ID: "works-orphan", ExternalID: "works-orphan", DisplayName: "WORKS 전용 조직"},
},
true,
)
require.Len(t, items, 4)
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-hanmac", items[1].WorksmobileParentID)
require.Equal(t, hanmac.Name, items[1].WorksmobileParentName)
require.Equal(t, "hanmac@hanmaceng.co.kr", items[1].WorksmobileParentEmail)
require.Equal(t, hanmac.ID, items[1].WorksmobileParentExternalKey)
require.Equal(t, "works-hanmac", items[1].BaronParentWorksmobileID)
require.Equal(t, hanmac.Name, items[1].BaronParentWorksmobileName)
require.Equal(t, "hanmac@hanmaceng.co.kr", items[1].BaronParentWorksmobileEmail)
require.Equal(t, userGroup.ID, items[2].BaronID)
require.Equal(t, "matched", items[2].Status)
require.Equal(t, "works-orphan", items[3].ExternalKey)
require.Equal(t, "missing_in_baron", items[3].Status)
}
func TestCompareWorksmobileGroupsShowsUserGroupMissingInWorksmobile(t *testing.T) {
parentID := "company-tenant"
userGroup := domain.Tenant{
ID: "team-tenant",
Name: "신규 팀",
Slug: "new-team",
Type: domain.TenantTypeUserGroup,
ParentID: &parentID,
}
items := compareWorksmobileGroups(
[]domain.Tenant{
{ID: parentID, Slug: "company", Name: "계열사", Type: domain.TenantTypeCompany},
userGroup,
},
nil,
false,
)
require.Len(t, items, 1)
require.Equal(t, userGroup.ID, items[0].BaronID)
require.Equal(t, "missing_in_worksmobile", items[0].Status)
}
func TestCompareWorksmobileGroupsMarksMatchedOrgUnitNeedsUpdate(t *testing.T) {
parentID := "parent-tenant"
tenant := domain.Tenant{
ID: "team-tenant",
Name: "변경된 팀명",
Slug: "team",
Type: domain.TenantTypeUserGroup,
ParentID: &parentID,
}
items := compareWorksmobileGroups(
[]domain.Tenant{
{ID: parentID, Slug: "parent", Name: "상위 조직", Type: domain.TenantTypeUserGroup},
tenant,
},
[]WorksmobileRemoteGroup{
{ID: "works-parent", ExternalID: parentID, DisplayName: "상위 조직"},
{ID: "works-team", ExternalID: tenant.ID, DisplayName: "이전 팀명", ParentID: "works-parent"},
},
false,
)
require.Len(t, items, 1)
require.Equal(t, tenant.ID, items[0].BaronID)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileGroupsCoversHanmacOrganizationRegressionIDs(t *testing.T) {
rootID := "038326b6-954a-48a7-a85f-efd83f62b82a"
samanID := "9caf62e1-297d-4e8f-870b-61780998bbeb"
hanmacID := "369c1843-56af-4344-9c21-0e01197ab861"
baronGroupID := "96369f12-6b66-4b2a-a916-d1c99d326f02"
changedID := "818c856b-9545-442f-b827-d1c569f200b0"
hanmacOnlyID := "2d217948-9c5a-42ea-805b-eef9c7421775"
baronOnlyID := "32464fd6-da51-473f-844a-ab88603ad1f0"
localTenants := []domain.Tenant{
{ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
{ID: samanID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID},
{ID: hanmacID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
{ID: baronGroupID, Slug: "baron-group", Name: "바론그룹", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
{ID: changedID, Slug: "rnd-saman", Name: "삼안기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &samanID},
{ID: hanmacOnlyID, Slug: "rnd-hanmac", Name: "한맥기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &hanmacID},
{ID: baronOnlyID, Slug: "rnd-baron", Name: "바론기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &baronGroupID},
}
remoteGroups := []WorksmobileRemoteGroup{
{ID: "works-saman", ExternalID: samanID, DisplayName: "삼안"},
{ID: "works-hanmac", ExternalID: hanmacID, DisplayName: "한맥기술"},
{
ID: "works-rnd-saman",
ExternalID: changedID,
DisplayName: "삼안기술개발센터(조직도용)",
},
}
items := compareWorksmobileGroups(localTenants, remoteGroups, false)
itemsByBaronID := map[string]WorksmobileComparisonItem{}
for _, item := range items {
itemsByBaronID[item.BaronID] = item
}
require.Equal(t, "needs_update", itemsByBaronID[changedID].Status)
require.Equal(t, "missing_in_worksmobile", itemsByBaronID[hanmacOnlyID].Status)
require.Equal(t, "missing_in_worksmobile", itemsByBaronID[baronOnlyID].Status)
}
func TestCompareWorksmobileGroupsDoesNotMatchBaronGroupOrganizationInGPDTDCDomain(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
rootID := "038326b6-954a-48a7-a85f-efd83f62b82a"
baronGroupID := "96369f12-6b66-4b2a-a916-d1c99d326f02"
orgID := "32464fd6-da51-473f-844a-ab88603ad1f0"
localTenants := []domain.Tenant{
{ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
{ID: baronGroupID, Slug: "baron-group", Name: "바론그룹", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
{ID: orgID, Slug: "rnd-baron", Name: "바론기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &baronGroupID},
}
remoteGroups := []WorksmobileRemoteGroup{
{
ID: "works-rnd-baron-gpdtdc",
ExternalID: orgID,
DisplayName: "바론기술개발센터(조직도용)",
DomainID: 1003,
DomainName: "총괄기획&기술개발센터",
},
}
items := compareWorksmobileGroups(localTenants, remoteGroups, false)
require.Len(t, items, 1)
require.Equal(t, orgID, items[0].BaronID)
require.Equal(t, "missing_in_worksmobile", items[0].Status)
require.Empty(t, items[0].WorksmobileID)
}
func TestCompareWorksmobileGroupsMatchesBaronGroupOrganizationInBaronGroupDomain(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
rootID := "038326b6-954a-48a7-a85f-efd83f62b82a"
baronGroupID := "96369f12-6b66-4b2a-a916-d1c99d326f02"
orgID := "32464fd6-da51-473f-844a-ab88603ad1f0"
localTenants := []domain.Tenant{
{ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
{ID: baronGroupID, Slug: "baron-group", Name: "바론그룹", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
{ID: orgID, Slug: "rnd-baron", Name: "바론기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &baronGroupID},
}
remoteGroups := []WorksmobileRemoteGroup{
{
ID: "works-rnd-baron",
ExternalID: orgID,
DisplayName: "바론기술개발센터(조직도용)",
DomainID: 1004,
DomainName: "바론그룹",
},
}
diffOnly := compareWorksmobileGroups(localTenants, remoteGroups, false)
all := compareWorksmobileGroups(localTenants, remoteGroups, true)
require.Empty(t, diffOnly)
require.Len(t, all, 1)
require.Equal(t, orgID, all[0].BaronID)
require.Equal(t, "matched", all[0].Status)
require.Equal(t, int64(1004), all[0].WorksmobileDomainID)
}
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)
require.Equal(t, 1, request.DisplayOrder)
}
func TestWorksmobileSyncServiceEnqueuesExternalKeyMissingOrgUnitDelete(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
client := &fakeWorksmobileDirectoryClient{
groups: []WorksmobileRemoteGroup{
{
ID: "works-org-1",
DisplayName: "WORKS 전용 조직",
DomainID: 1001,
ParentID: "works-parent",
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
client,
)
item, err := service.EnqueueOrgUnitDelete(context.Background(), rootID, "works-org-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionDelete, outboxRepo.created[0].Action)
require.Equal(t, "works-org-1", outboxRepo.created[0].Payload["worksmobileId"])
}
func TestWorksmobileSyncServiceEnqueuesExternalKeyPresentWorksOnlyOrgUnitDelete(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
client := &fakeWorksmobileDirectoryClient{
groups: []WorksmobileRemoteGroup{
{
ID: "works-org-1",
ExternalID: "baron-tenant-1",
ParentID: "works-parent",
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
client,
)
item, err := service.EnqueueOrgUnitDelete(context.Background(), rootID, "works-org-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionDelete, outboxRepo.created[0].Action)
require.Equal(t, "baron-tenant-1", outboxRepo.created[0].Payload["externalKey"])
}
func TestWorksmobileSyncServiceReconcilesWorksOnlyOrgUnitBySlugLocalPart(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1001")
rootID := "root-tenant"
orgID := "baron-org-1"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
organization := domain.Tenant{
ID: orgID,
Slug: "tech-dev-center",
Name: "기술개발센터",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
client := &fakeWorksmobileDirectoryClient{
groups: []WorksmobileRemoteGroup{
{
ID: "works-org-1",
ExternalID: "legacy-external-key",
DisplayName: "기술개발센터",
MailLocalPart: "tech-dev-center",
DomainID: 1001,
ParentID: "works-parent",
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, orgID: organization},
list: []domain.Tenant{root, organization},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
client,
)
item, err := service.EnqueueOrgUnitDelete(context.Background(), rootID, "works-org-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionUpsert, outboxRepo.created[0].Action)
require.Equal(t, orgID, outboxRepo.created[0].ResourceID)
request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.Equal(t, orgID, request.OrgUnitExternalKey)
require.Equal(t, "tech-dev-center", outboxRepo.created[0].Payload["matchLocalPart"])
}
func TestCompareWorksmobileGroupsFillsParentDisplayFromBaronParentMatch(t *testing.T) {
rootID := "root-tenant"
parent := domain.Tenant{
ID: "parent-tenant",
Name: "삼안",
Slug: "saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
}
child := domain.Tenant{
ID: "child-tenant",
Name: "업무",
Slug: "operations",
Type: domain.TenantTypeOrganization,
ParentID: &parent.ID,
}
items := compareWorksmobileGroups(
[]domain.Tenant{
{ID: rootID, Name: "한맥가족", Slug: HanmacFamilyTenantSlug, Type: domain.TenantTypeCompanyGroup},
parent,
child,
},
[]WorksmobileRemoteGroup{
{ID: "works-parent", ExternalID: parent.ID, DisplayName: "삼안", Email: "saman@samaneng.com"},
{ID: "works-child", ExternalID: child.ID, DisplayName: "업무", ParentID: "works-parent"},
},
true,
)
require.Len(t, items, 1)
require.Equal(t, child.ID, items[0].BaronID)
require.Equal(t, "works-parent", items[0].WorksmobileParentID)
require.Equal(t, "삼안", items[0].WorksmobileParentName)
require.Equal(t, "saman@samaneng.com", items[0].WorksmobileParentEmail)
require.Equal(t, parent.ID, items[0].WorksmobileParentExternalKey)
}
func TestWorksmobileSyncServiceReconcilesTopLevelWorksOnlyOrgUnitBeforeProtectedDeleteGuard(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
orgID := "baron-operations"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
samanID := "saman-tenant"
saman := domain.Tenant{
ID: samanID,
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
organization := domain.Tenant{
ID: orgID,
Slug: "operations",
Name: "업무",
Type: domain.TenantTypeOrganization,
ParentID: &samanID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
client := &fakeWorksmobileDirectoryClient{
groups: []WorksmobileRemoteGroup{
{
ID: "works-operations",
ExternalID: "legacy-operations-id",
DisplayName: "업무팀",
Email: "operations@samaneng.com",
MailLocalPart: "operations",
DomainID: 1001,
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, samanID: saman, orgID: organization},
list: []domain.Tenant{root, saman, organization},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
client,
)
item, err := service.EnqueueOrgUnitDelete(context.Background(), rootID, "works-operations")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionUpsert, outboxRepo.created[0].Action)
require.Equal(t, orgID, outboxRepo.created[0].ResourceID)
request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.Equal(t, orgID, request.OrgUnitExternalKey)
require.Equal(t, "operations", outboxRepo.created[0].Payload["matchLocalPart"])
}
func TestWorksmobileSyncServiceRejectsProtectedDomainRootOrgUnitDelete(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
client := &fakeWorksmobileDirectoryClient{
groups: []WorksmobileRemoteGroup{
{
ID: "works-root",
DisplayName: "한맥기술",
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
client,
)
item, err := service.EnqueueOrgUnitDelete(context.Background(), rootID, "works-root")
require.Nil(t, item)
require.Error(t, err)
require.Contains(t, err.Error(), "protected worksmobile domain root")
require.Empty(t, outboxRepo.created)
}
func TestWorksmobileSyncServiceTreatsHanmacFamilyChildCompaniesAsDomainRoots(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("HANMAC_DOMAIN_ID", "1002")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
tests := []struct {
name string
company domain.Tenant
organization domain.Tenant
wantDomainID int64
wantEmail string
}{
{
name: "saman",
company: domain.Tenant{
ID: "company-saman",
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
},
organization: domain.Tenant{
ID: "org-saman-planning",
Slug: "saman-planning",
Name: "삼안 기획팀",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1001,
wantEmail: "saman-planning@samaneng.com",
},
{
name: "hanmac",
company: domain.Tenant{
ID: "company-hanmac",
Slug: "hanmac",
Name: "한맥기술",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "hanmaceng.co.kr"}},
},
organization: domain.Tenant{
ID: "org-hanmac-planning",
Slug: "hanmac-planning",
Name: "한맥 기획팀",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1002,
wantEmail: "hanmac-planning@hanmaceng.co.kr",
},
{
name: "gpdtdc",
company: domain.Tenant{
ID: "company-gpdtdc",
Slug: "gpdtdc",
Name: "총괄기획&기술개발센터",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
},
organization: domain.Tenant{
ID: "org-gpdtdc-planning",
Slug: "gpdtdc-planning",
Name: "총괄 기획팀",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1003,
wantEmail: "gpdtdc-planning@baroncs.co.kr",
},
{
name: "baron-group",
company: domain.Tenant{
ID: "company-barongroup",
Slug: "baron-group",
Name: "바론그룹",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
},
organization: domain.Tenant{
ID: "org-baron-planning",
Slug: "baron-planning",
Name: "바론 기획팀",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1004,
wantEmail: "baron-planning@brsw.kr",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
organization := tt.organization
organization.ParentID = &tt.company.ID
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{
rootID: root,
tt.company.ID: tt.company,
organization.ID: organization,
},
list: []domain.Tenant{root, tt.company, organization},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, organization.ID)
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.Equal(t, organization.ID, request.OrgUnitExternalKey)
require.Equal(t, tt.wantDomainID, request.DomainID)
require.Equal(t, tt.wantEmail, request.Email)
require.Empty(t, request.ParentOrgUnitID)
})
}
}
func TestWorksmobileSyncServiceUsesBaronGroupDomainForBaronGroupChildOrganization(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
rootID := "038326b6-954a-48a7-a85f-efd83f62b82a"
baronGroupID := "96369f12-6b66-4b2a-a916-d1c99d326f02"
orgID := "32464fd6-da51-473f-844a-ab88603ad1f0"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
Type: domain.TenantTypeCompanyGroup,
}
baronGroup := domain.Tenant{
ID: baronGroupID,
Slug: "baron-group",
Name: "바론그룹",
Type: domain.TenantTypeCompanyGroup,
ParentID: &rootID,
}
organization := domain.Tenant{
ID: orgID,
Slug: "rnd-baron",
Name: "바론기술개발센터(조직도용)",
Type: domain.TenantTypeOrganization,
ParentID: &baronGroupID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{
root.ID: root,
baronGroup.ID: baronGroup,
organization.ID: organization,
},
list: []domain.Tenant{root, baronGroup, organization},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, organization.ID)
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.Equal(t, int64(1004), request.DomainID)
require.Equal(t, "rnd-baron@brsw.kr", request.Email)
}
func TestWorksmobileSyncServiceUsesGPDTDCDomainForGPDTDCChildOrganization(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
t.Setenv("BARONGROUP_DOMAIN_ID", "1004")
rootID := "038326b6-954a-48a7-a85f-efd83f62b82a"
gpdtdcID := "5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee"
orgID := "gpdtdc-child-organization"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
Type: domain.TenantTypeCompanyGroup,
}
gpdtdc := domain.Tenant{
ID: gpdtdcID,
Slug: "gpdtdc",
Name: "총괄기획&기술개발센터",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
}
organization := domain.Tenant{
ID: orgID,
Slug: "planning",
Name: "기획",
Type: domain.TenantTypeOrganization,
ParentID: &gpdtdcID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{
root.ID: root,
gpdtdc.ID: gpdtdc,
organization.ID: organization,
},
list: []domain.Tenant{root, gpdtdc, organization},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, organization.ID)
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
request := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.Equal(t, int64(1003), request.DomainID)
require.Equal(t, "planning@baroncs.co.kr", request.Email)
}
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)
}
func TestWorksmobileSyncServiceSkipsArchivedUsersInComparison(t *testing.T) {
rootID := "root-tenant"
companyID := "company-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
company := domain.Tenant{
ID: companyID,
Name: "계열사",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
}
archived := domain.User{
ID: "archived-user",
Email: "archived@samaneng.com",
Name: "Archived",
TenantID: &companyID,
Status: domain.UserStatusArchived,
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
&fakeWorksmobileUserRepo{byTenant: []domain.User{archived}},
&fakeWorksmobileOutboxRepo{},
&fakeWorksmobileDirectoryClient{users: []WorksmobileRemoteUser{{
ID: "works-archived",
ExternalID: archived.ID,
Email: archived.Email,
}}},
)
comparison, err := service.GetComparison(context.Background(), rootID, true)
require.NoError(t, err)
require.Empty(t, comparison.Users)
}
func TestWorksmobileSyncServiceBackfillDryRunSkipsArchivedUsers(t *testing.T) {
rootID := "root-tenant"
companyID := "company-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
company := domain.Tenant{
ID: companyID,
Name: "계열사",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
}
active := domain.User{
ID: "active-user",
Email: "active@samaneng.com",
Name: "Active",
TenantID: &companyID,
Status: domain.UserStatusActive,
}
archived := domain.User{
ID: "archived-user",
Email: "archived@samaneng.com",
Name: "Archived",
TenantID: &companyID,
Status: domain.UserStatusArchived,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
&fakeWorksmobileUserRepo{byTenant: []domain.User{active, archived}},
outboxRepo,
nil,
)
dryRun, err := service.EnqueueBackfillDryRun(context.Background(), rootID)
require.NoError(t, err)
require.Equal(t, 1, dryRun.UserCount)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, 1, outboxRepo.created[0].Payload["userCount"])
}
func TestCompareWorksmobileUsersMarksManagerChangeNeedsUpdate(t *testing.T) {
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-manager",
Email: "manager@samaneng.com",
Name: "Manager User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": tenantID,
"isPrimary": true,
"isManager": true,
},
},
},
}
remoteManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-manager",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "externalKey:" + tenantID,
PrimaryOrgUnitIsManager: &remoteManager,
}},
true,
map[string]domain.Tenant{
tenantID: {ID: tenantID, Name: "Leaf", Type: domain.TenantTypeOrganization},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileUsersMarksSecondaryManagerChangeNeedsUpdate(t *testing.T) {
primaryTenantID := "tenant-company"
secondaryTenantID := "tenant-gpdtdc-leaf"
user := domain.User{
ID: "user-secondary-manager",
Email: "secondary-manager@samaneng.com",
Name: "Secondary Manager User",
TenantID: &secondaryTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": primaryTenantID,
"isPrimary": true,
},
map[string]any{
"tenantId": secondaryTenantID,
"isPrimary": false,
"isManager": true,
},
},
},
}
remotePrimaryManager := false
remoteSecondaryManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-secondary-manager",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "externalKey:" + primaryTenantID,
PrimaryOrgUnitIsManager: &remotePrimaryManager,
OrgUnitManagers: map[string]*bool{
"externalKey:" + primaryTenantID: &remotePrimaryManager,
"externalKey:" + secondaryTenantID: &remoteSecondaryManager,
},
}},
true,
map[string]domain.Tenant{
primaryTenantID: {ID: primaryTenantID, Name: "Company", Type: domain.TenantTypeCompany},
secondaryTenantID: {ID: secondaryTenantID, Name: "GPDTDC Leaf", Type: domain.TenantTypeOrganization},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileUsersMarksMissingSecondaryOrganizationNeedsUpdate(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
primaryTenantID := "tenant-saman"
gpdtdcID := "tenant-gpdtdc"
secondaryTenantID := "tenant-gpdtdc-leaf"
user := domain.User{
ID: "user-secondary-org",
Email: "secondary-org@samaneng.com",
Name: "Secondary Org User",
TenantID: &secondaryTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": primaryTenantID,
"isPrimary": true,
},
map[string]any{
"tenantId": secondaryTenantID,
"isPrimary": false,
},
},
},
}
remotePrimaryManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-secondary-org",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + primaryTenantID,
Primary: true,
},
},
},
},
PrimaryOrgUnitID: "externalKey:" + primaryTenantID,
PrimaryOrgUnitIsManager: &remotePrimaryManager,
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
primaryTenantID: {ID: primaryTenantID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
secondaryTenantID: {ID: secondaryTenantID, Slug: "people-growth", Name: "인재성장", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileUsersMarksPhoneAndEmployeeNumberChangesNeedsUpdate(t *testing.T) {
tenantID := "tenant-saman"
user := domain.User{
ID: "user-phone-employee",
Email: "phone-employee@samaneng.com",
Name: "Phone Employee User",
Phone: "010-1234-5678",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"employeeNumber": "EMP001",
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-phone-employee",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
CellPhone: "+821099998888",
EmployeeNumber: "EMP999",
}},
true,
map[string]domain.Tenant{
tenantID: {ID: tenantID, Name: "삼안", Type: domain.TenantTypeCompany},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
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, tenantSlug 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
}
func (f *fakeWorksmobileUserRepo) DB() *gorm.DB {
return nil
}