1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/worksmobile_sync_service_test.go
2026-06-22 17:56:20 +09:00

3584 lines
117 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"
userGroupID := "saman-user-group"
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"}},
}
userGroup := domain.Tenant{
ID: userGroupID,
Slug: "saman-team",
Name: "Saman Team",
Type: domain.TenantTypeUserGroup,
ParentID: &tenantID,
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusSuspended,
TenantID: &userGroupID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant, userGroupID: userGroup},
list: []domain.Tenant{root, tenant, userGroup},
},
&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", "InputPass1!")
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"])
require.Equal(t, "InputPass1!", outboxRepo.created[0].Payload["initialPassword"])
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileUserPayload)
require.True(t, ok)
require.Equal(t, "ADMIN", request.PasswordConfig.PasswordCreationType)
require.Equal(t, "InputPass1!", request.PasswordConfig.Password)
require.NotNil(t, request.PasswordConfig.ChangePasswordAtNextLogin)
require.True(t, *request.PasswordConfig.ChangePasswordAtNextLogin)
}
func TestWorksmobileSyncServiceSkipsAdminInitialPasswordWhenEmpty(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)
initialPassword := stringValue(outboxRepo.created[0].Payload["initialPassword"])
require.Empty(t, initialPassword)
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileUserPayload)
require.True(t, ok)
require.Empty(t, request.PasswordConfig.PasswordCreationType)
require.Empty(t, request.PasswordConfig.Password)
require.Nil(t, request.PasswordConfig.ChangePasswordAtNextLogin)
require.Empty(t, stringValue(outboxRepo.created[0].Payload["credentialBatchId"]))
}
func TestWorksmobileSyncServiceCreatesDistinctUserSyncHistoryJobs(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,
)
first, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
require.NoError(t, err)
second, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
require.NoError(t, err)
require.NotNil(t, first)
require.NotNil(t, second)
require.Len(t, outboxRepo.created, 2)
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
}
func TestWorksmobileSyncServiceCreatesDistinctAutomaticUserSyncHistoryJobs(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,
)
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
require.Len(t, outboxRepo.created, 2)
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
}
func TestWorksmobileSyncServiceUserUpdateIsUpdateOnlyWithoutInitialPassword(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,
)
require.NoError(t, service.EnqueueUserUpdateIfInScope(context.Background(), target))
require.Len(t, outboxRepo.created, 1)
require.Equal(t, "update_only", outboxRepo.created[0].Payload["provisioningMode"])
require.NotContains(t, outboxRepo.created[0].Payload, "initialPassword")
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileUserPayload)
require.True(t, ok)
require.True(t, request.PasswordConfig.IsZero())
}
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 TestWorksmobileSyncServicePasswordResetAllowsExcludedPrimaryTenant(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
excludedOrgID := "excluded-org"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
excludedOrg := domain.Tenant{
ID: excludedOrgID,
Slug: "excluded-team",
Name: "Excluded Team",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
Config: domain.JSONMap{"worksmobileExcluded": true},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
TenantID: &excludedOrgID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, excludedOrgID: excludedOrg}, list: []domain.Tenant{root, excludedOrg}},
&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, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
require.Equal(t, "Target", outboxRepo.created[0].Payload["displayName"])
require.Empty(t, outboxRepo.created[0].Payload["primaryLeafOrgName"])
}
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{
"tenantRootId": root.ID,
"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{
"tenantRootId": root.ID,
"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 TestWorksmobileSyncServiceOverviewScopesRecentJobsToTenantRoot(t *testing.T) {
rootID := "root-tenant"
childID := "child-org"
otherRootID := "other-root"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
child := domain.Tenant{
ID: childID,
Slug: "structure-planning",
Name: "구조물계획",
Type: domain.TenantTypeUserGroup,
ParentID: &rootID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{
recent: []domain.WorksmobileOutbox{
{
ID: "job-root-user-failed",
ResourceType: domain.WorksmobileResourceUser,
ResourceID: "user-1",
Status: domain.WorksmobileOutboxStatusFailed,
Payload: domain.JSONMap{"tenantRootId": rootID},
LastError: "worksmobile api failed",
},
{
ID: "job-child-org-legacy",
ResourceType: domain.WorksmobileResourceOrgUnit,
ResourceID: childID,
Status: domain.WorksmobileOutboxStatusFailed,
LastError: "legacy org job without tenantRootId",
},
{
ID: "job-other-root",
ResourceType: domain.WorksmobileResourceUser,
ResourceID: "user-2",
Status: domain.WorksmobileOutboxStatusFailed,
Payload: domain.JSONMap{"tenantRootId": otherRootID},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, childID: child},
list: []domain.Tenant{child},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
overview, err := service.GetTenantOverview(context.Background(), rootID)
require.NoError(t, err)
require.Len(t, overview.RecentJobs, 2)
require.Equal(t, "job-root-user-failed", overview.RecentJobs[0].ID)
require.Equal(t, "job-child-org-legacy", overview.RecentJobs[1].ID)
require.Equal(t, "legacy org job without tenantRootId", overview.RecentJobs[1].LastError)
}
func TestWorksmobileSyncServiceEnqueueTenantUpsertReflectsChangedParentOrgUnit(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
companyID := "saman-tenant"
newParentID := "new-parent-org"
childID := "child-org"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
Type: domain.TenantTypeCompanyGroup,
}
company := domain.Tenant{
ID: companyID,
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
newParent := domain.Tenant{
ID: newParentID,
Slug: "planning",
Name: "총괄기획",
Type: domain.TenantTypeOrganization,
ParentID: &companyID,
}
child := domain.Tenant{
ID: childID,
Slug: "people-growth",
Name: "인재성장",
Type: domain.TenantTypeOrganization,
ParentID: &newParentID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{
rootID: root,
companyID: company,
newParentID: newParent,
childID: child,
},
list: []domain.Tenant{root, company, newParent, child},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
err := service.EnqueueTenantUpsertIfInScope(context.Background(), child)
require.NoError(t, err)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileResourceOrgUnit, outboxRepo.created[0].ResourceType)
require.Equal(t, domain.WorksmobileActionUpsert, outboxRepo.created[0].Action)
require.Equal(t, childID, outboxRepo.created[0].ResourceID)
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileOrgUnitPayload)
require.True(t, ok)
require.Equal(t, childID, request.OrgUnitExternalKey)
require.Equal(t, "externalKey:"+newParentID, request.ParentOrgUnitID)
require.Equal(t, "people-growth", outboxRepo.created[0].Payload["matchLocalPart"])
}
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.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileResourceOrgUnit, outboxRepo.created[0].ResourceType)
require.Equal(t, domain.WorksmobileOutboxStatusFailed, outboxRepo.created[0].Status)
require.Equal(t, companyID, outboxRepo.created[0].ResourceID)
require.Equal(t, rootID, outboxRepo.created[0].Payload["tenantRootId"])
require.Equal(t, "target tenant is not a worksmobile orgunit tenant", outboxRepo.created[0].LastError)
}
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 TestWorksmobileSyncServiceDeletesWorksOrgUnitEvenWhenSlugLocalPartMatches(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.WorksmobileActionDelete, outboxRepo.created[0].Action)
require.Equal(t, "works-org-1", outboxRepo.created[0].ResourceID)
require.Equal(t, "works-org-1", outboxRepo.created[0].Payload["worksmobileId"])
require.Equal(t, "legacy-external-key", outboxRepo.created[0].Payload["externalKey"])
}
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.WorksmobileActionDelete, outboxRepo.created[0].Action)
require.Equal(t, "works-operations", outboxRepo.created[0].ResourceID)
require.Equal(t, "works-operations", outboxRepo.created[0].Payload["worksmobileId"])
require.Equal(t, "legacy-operations-id", outboxRepo.created[0].Payload["externalKey"])
}
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 TestWorksmobileSyncServiceDeletesPendingJobsForTenantRoot(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
outboxRepo := &fakeWorksmobileOutboxRepo{deletedPendingCount: 2}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
result, err := service.DeletePendingJobs(context.Background(), rootID)
require.NoError(t, err)
require.Equal(t, 2, result.DeletedCount)
require.Equal(t, rootID, outboxRepo.deletedPendingTenantRootID)
}
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("HALLA_DOMAIN_ID", "1005")
t.Setenv("WORKS_DEFAULT_DOMAIN_HALLA", "hallasanup.com")
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",
},
{
name: "halla",
company: domain.Tenant{
ID: "company-halla",
Slug: "halla",
Name: "한라산업개발",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "hallasanup.com"}},
},
organization: domain.Tenant{
ID: "org-halla-planning",
Slug: "halla-planning",
Name: "한라 기획팀",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1005,
wantEmail: "halla-planning@hallasanup.com",
},
{
name: "hanlla legacy slug",
company: domain.Tenant{
ID: "company-hanlla",
Slug: "hanlla",
Name: "한라산업개발",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
},
organization: domain.Tenant{
ID: "org-hanlla-construction-sites",
Slug: "hanlla-construction-sites",
Name: "시공현장",
Type: domain.TenantTypeOrganization,
},
wantDomainID: 1005,
wantEmail: "hanlla-construction-sites@hallasanup.com",
},
}
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 TestWorksmobileSyncServiceGetComparisonUsesIdentityMirrorWhenReady(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,
}
userRepo := &fakeWorksmobileUserRepo{byTenant: []domain.User{{
ID: "local-only-user",
Email: "local-only@example.com",
Name: "Local Only",
TenantID: &companyID,
Status: domain.UserStatusActive,
}}}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
userRepo,
&fakeWorksmobileOutboxRepo{},
&fakeWorksmobileDirectoryClient{users: []WorksmobileRemoteUser{{
ID: "works-user",
ExternalID: "mirror-user",
Email: "mirror@example.com",
DisplayName: "Mirror User",
PrimaryOrgUnitID: "externalKey:" + companyID,
}}},
)
service.SetIdentityMirror(&fakeWorksmobileIdentityMirror{
status: domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
MirrorVersion: "kratos-full-pagination-v1",
ObservedCount: 1,
},
identities: []KratosIdentity{{
ID: "mirror-user",
State: "active",
Traits: map[string]any{
"email": "mirror@example.com",
"name": "Mirror User",
"tenant_id": companyID,
"role": domain.RoleUser,
"affiliationType": "internal",
},
}},
})
comparison, err := service.GetComparison(context.Background(), rootID, true)
require.NoError(t, err)
require.Empty(t, userRepo.requestedTenantIDs)
require.Len(t, comparison.Users, 1)
require.Equal(t, "matched", comparison.Users[0].Status)
require.Equal(t, "mirror-user", comparison.Users[0].BaronID)
require.Equal(t, "mirror-user", comparison.Users[0].ExternalKey)
}
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 TestWorksmobileSyncServiceSkipsSoftDeletedUsersInComparison(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,
}
deleted := domain.User{
ID: "deleted-user",
Email: "deleted@samaneng.com",
Name: "Deleted",
TenantID: &companyID,
Status: domain.UserStatusActive,
DeletedAt: gorm.DeletedAt{
Time: time.Now(),
Valid: true,
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, companyID: company}, list: []domain.Tenant{root, company}},
&fakeWorksmobileUserRepo{byTenant: []domain.User{deleted}},
&fakeWorksmobileOutboxRepo{},
&fakeWorksmobileDirectoryClient{users: []WorksmobileRemoteUser{{
ID: "works-deleted",
ExternalID: deleted.ID,
Email: deleted.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 TestWorksmobileSyncServiceBackfillDryRunSkipsWorksmobileExcludedSubtree(t *testing.T) {
rootID := "root-tenant"
excludedCompanyID := "excluded-company"
excludedOrgID := "excluded-org"
includedCompanyID := "included-company"
includedOrgID := "included-org"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
excludedCompany := domain.Tenant{
ID: excludedCompanyID,
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Config: domain.JSONMap{"worksmobileExcluded": true},
}
excludedOrg := domain.Tenant{
ID: excludedOrgID,
Slug: "excluded-team",
Name: "제외팀",
Type: domain.TenantTypeOrganization,
ParentID: &excludedCompanyID,
}
includedCompany := domain.Tenant{
ID: includedCompanyID,
Slug: "halla",
Name: "한라산업개발",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
}
includedOrg := domain.Tenant{
ID: includedOrgID,
Slug: "included-team",
Name: "연동팀",
Type: domain.TenantTypeOrganization,
ParentID: &includedCompanyID,
}
excludedUser := domain.User{
ID: "excluded-user",
Email: "excluded@samaneng.com",
Name: "Excluded User",
TenantID: &excludedOrgID,
Status: domain.UserStatusActive,
}
includedUser := domain.User{
ID: "included-user",
Email: "included@hallasanup.com",
Name: "Included User",
TenantID: &includedOrgID,
Status: domain.UserStatusActive,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{
rootID: root,
excludedCompanyID: excludedCompany,
excludedOrgID: excludedOrg,
includedCompanyID: includedCompany,
includedOrgID: includedOrg,
},
list: []domain.Tenant{root, excludedCompany, excludedOrg, includedCompany, includedOrg},
},
&fakeWorksmobileUserRepo{byTenant: []domain.User{excludedUser, includedUser}},
outboxRepo,
nil,
)
dryRun, err := service.EnqueueBackfillDryRun(context.Background(), rootID)
require.NoError(t, err)
require.Equal(t, 1, dryRun.OrgUnitCount)
require.Equal(t, 1, dryRun.UserCount)
require.Len(t, outboxRepo.created, 1)
require.ElementsMatch(t, []string{includedOrgID}, outboxRepo.created[0].Payload["tenantIds"])
require.Equal(t, 1, outboxRepo.created[0].Payload["userCount"])
}
func TestWorksmobileSyncServiceRejectsExcludedOrgUnitSync(t *testing.T) {
rootID := "root-tenant"
excludedCompanyID := "excluded-company"
excludedOrgID := "excluded-org"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
excludedCompany := domain.Tenant{
ID: excludedCompanyID,
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Config: domain.JSONMap{"worksmobileExcluded": true},
}
excludedOrg := domain.Tenant{
ID: excludedOrgID,
Slug: "excluded-team",
Name: "제외팀",
Type: domain.TenantTypeOrganization,
ParentID: &excludedCompanyID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, excludedCompanyID: excludedCompany, excludedOrgID: excludedOrg},
list: []domain.Tenant{root, excludedCompany, excludedOrg},
},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
item, err := service.EnqueueOrgUnitSync(context.Background(), rootID, excludedOrgID)
require.Nil(t, item)
require.ErrorContains(t, err, "excluded from Worksmobile sync")
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileResourceOrgUnit, outboxRepo.created[0].ResourceType)
require.Equal(t, domain.WorksmobileOutboxStatusFailed, outboxRepo.created[0].Status)
require.Equal(t, excludedOrgID, outboxRepo.created[0].ResourceID)
require.Equal(t, rootID, outboxRepo.created[0].Payload["tenantRootId"])
require.Equal(t, "excluded-team", outboxRepo.created[0].Payload["matchLocalPart"])
require.Equal(t, "target tenant is excluded from Worksmobile sync", outboxRepo.created[0].LastError)
}
func TestWorksmobileSyncServiceSkipsExcludedTenantAndUserEventSync(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
excludedCompanyID := "excluded-company"
excludedOrgID := "excluded-org"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "한맥가족",
}
excludedCompany := domain.Tenant{
ID: excludedCompanyID,
Slug: "saman",
Name: "삼안",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Config: domain.JSONMap{"worksmobileExcluded": true},
}
excludedOrg := domain.Tenant{
ID: excludedOrgID,
Slug: "excluded-team",
Name: "제외팀",
Type: domain.TenantTypeOrganization,
ParentID: &excludedCompanyID,
}
user := domain.User{
ID: "excluded-user",
Email: "excluded@samaneng.com",
Name: "Excluded User",
TenantID: &excludedOrgID,
Status: domain.UserStatusActive,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{
tenants: map[string]domain.Tenant{rootID: root, excludedCompanyID: excludedCompany, excludedOrgID: excludedOrg},
list: []domain.Tenant{root, excludedCompany, excludedOrg},
},
&fakeWorksmobileUserRepo{byID: map[string]domain.User{user.ID: user}},
outboxRepo,
nil,
)
require.NoError(t, service.EnqueueTenantUpsertIfInScope(context.Background(), excludedOrg))
require.NoError(t, service.EnqueueTenantDeleteIfInScope(context.Background(), excludedOrg))
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), user))
item, err := service.EnqueueUserSync(context.Background(), rootID, user.ID, "", "")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileResourceUser, outboxRepo.created[0].ResourceType)
require.Equal(t, user.ID, outboxRepo.created[0].ResourceID)
require.Equal(t, domain.WorksmobileActionUpsert, outboxRepo.created[0].Action)
require.Empty(t, outboxRepo.created[0].Status)
require.Empty(t, outboxRepo.created[0].LastError)
require.Equal(t, rootID, outboxRepo.created[0].Payload["tenantRootId"])
require.Equal(t, user.Email, outboxRepo.created[0].Payload["loginEmail"])
require.Equal(t, user.Name, outboxRepo.created[0].Payload["displayName"])
require.Empty(t, outboxRepo.created[0].Payload["primaryLeafOrgName"])
request, ok := outboxRepo.created[0].Payload["request"].(WorksmobileUserPayload)
require.True(t, ok)
require.Empty(t, request.Organizations)
}
func TestCompareWorksmobileUsersMarksManagerChangeNeedsUpdate(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-saman"
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,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + tenantID,
Primary: true,
IsManager: &remoteManager,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &rootID},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileUsersMarksTenantLinkedGradeChangeNeedsUpdate(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
gpdtdcID := "tenant-gpdtdc"
tenantID := "tenant-gpdtdc-leaf"
user := domain.User{
ID: "user-grade",
Email: "grade@samaneng.com",
Name: "Grade User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": tenantID,
"isPrimary": true,
"grade": "책임",
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-grade",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
LevelName: "",
PrimaryOrgUnitID: "externalKey:" + tenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1003,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + tenantID, Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
tenantID: {ID: tenantID, Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
require.Contains(t, items[0].UpdateReasons, "grade")
require.Equal(t, "책임 연구원", items[0].BaronGrade)
}
func TestCompareWorksmobileUsersIncludesMembershipMatchForGradeUpdate(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
gpdtdcID := "tenant-gpdtdc"
hmegID := "1d74bebb-c5a1-49d4-bec4-90f0c89ad21f"
user := domain.User{
ID: "user-hmeg-researcher",
Email: "hmeg-researcher@baroncs.co.kr",
Name: "HMEG Researcher",
TenantID: &hmegID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": hmegID,
"isPrimary": true,
"grade": "책임연구원",
},
},
},
}
items := compareWorksmobileUsersWithRemoteGroups(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-hmeg-researcher",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1003,
PrimaryOrgUnitID: "works-hmeg",
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1003,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "works-hmeg", Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
hmegID: {ID: hmegID, Slug: "hmeg", Name: "HmEG", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
[]WorksmobileRemoteGroup{{
ID: "works-hmeg",
ExternalID: hmegID,
DisplayName: "WORKS HmEG",
DomainID: 1003,
DomainName: "baroncs.co.kr",
}},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
require.Contains(t, items[0].UpdateReasons, "grade")
require.Len(t, items[0].UserMemberships, 1)
require.Equal(t, hmegID, items[0].UserMemberships[0].BaronOrgID)
require.Equal(t, "HmEG", items[0].UserMemberships[0].BaronOrgName)
require.Equal(t, "hmeg", items[0].UserMemberships[0].BaronOrgSlug)
require.Equal(t, "책임 연구원", items[0].UserMemberships[0].BaronGrade)
require.Equal(t, "works-hmeg", items[0].UserMemberships[0].WorksmobileOrgID)
require.Equal(t, "WORKS HmEG", items[0].UserMemberships[0].WorksmobileOrgName)
require.True(t, items[0].UserMemberships[0].GradeNeedsUpdate)
}
func TestCompareWorksmobileUsersDoesNotUpdateWhenWORKSMembershipIsBaronSubset(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
companyID := "tenant-saman"
tenantID := "tenant-saman-leaf"
gpdtdcID := "tenant-gpdtdc"
gpdtdcTenantID := "tenant-is-3"
user := domain.User{
ID: "user-gpdtdc-grade-with-saman-works-org",
Email: "gpdtdc-grade@samaneng.com",
Name: "Research Grade User",
TenantID: &gpdtdcTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": tenantID,
"isPrimary": true,
"grade": "팀장",
},
map[string]any{
"tenantId": gpdtdcTenantID,
"grade": "책임",
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-gpdtdc-grade",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
LevelName: "팀장",
PrimaryOrgUnitID: "externalKey:" + tenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + tenantID, Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Slug: "saman-leaf", Name: "삼안 조직", Type: domain.TenantTypeOrganization, ParentID: &companyID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
gpdtdcTenantID: {ID: gpdtdcTenantID, Slug: "is-3", Name: "기술기획", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
require.NotContains(t, items[0].UpdateReasons, "grade")
require.NotContains(t, items[0].UpdateReasons, "organization")
}
func TestCompareWorksmobileUsersDoesNotCompareGPDTDCGradeAgainstSamanOrgChartLevel(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "300285955")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
samanID := "045e0b22-fae7-4229-1724-039c5af16849"
samanOrgChartID := "97a7e34d-2042-4793-27dc-03ffd68db801"
gpdtdcID := "tenant-gpdtdc"
infraBIM1ID := "432b5261-421b-4e5f-914f-32d7d22fd01f"
user := domain.User{
ID: "abaf0788-2d68-4b7d-b40a-c0251f38ae21",
Email: "hwan@samaneng.com",
Name: "안효원",
TenantID: &infraBIM1ID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": samanOrgChartID,
"grade": "선임연구원",
},
map[string]any{
"tenantId": infraBIM1ID,
"isPrimary": true,
"grade": "선임연구원",
},
},
},
}
remoteManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "045e0b22-fae7-4229-1724-039c5af16849",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 300285955,
PrimaryOrgUnitID: "externalKey:" + samanOrgChartID,
PrimaryOrgUnitName: "삼안기술개발센터(조직도용)",
PrimaryOrgUnitIsManager: &remoteManager,
PrimaryOrgUnitPositionName: "조직장 아님",
Organizations: []WorksmobileUserOrganization{
{
DomainID: 300285955,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{
OrgUnitID: "externalKey:" + samanOrgChartID,
Primary: true,
IsManager: &remoteManager,
}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
samanID: {ID: samanID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
samanOrgChartID: {ID: samanOrgChartID, Slug: "rnd-saman", Name: "삼안기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &samanID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
infraBIM1ID: {ID: infraBIM1ID, Slug: "infra-bim1", Name: "인프라 BIM1", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
require.NotContains(t, items[0].UpdateReasons, "grade")
}
func TestCompareWorksmobileUsersUsesPrimaryAppointmentForGPDTDCGrade(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
companyID := "tenant-saman"
orgChartTenantID := "tenant-rnd-saman"
gpdtdcID := "tenant-gpdtdc"
gpdtdcTenantID := "tenant-is-3"
user := domain.User{
ID: "user-orgchart-grade",
Email: "orgchart-grade@samaneng.com",
Name: "Orgchart Grade User",
TenantID: &gpdtdcTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": orgChartTenantID,
},
map[string]any{
"tenantId": gpdtdcTenantID,
"isPrimary": true,
"grade": "수석연구원",
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-orgchart-grade",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
LevelName: "수석 연구원",
PrimaryOrgUnitID: "externalKey:" + gpdtdcTenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: false,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + orgChartTenantID, Primary: true}},
},
{
DomainID: 1003,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + gpdtdcTenantID, Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
orgChartTenantID: {ID: orgChartTenantID, Slug: "rnd-saman", Name: "삼안기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &companyID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
gpdtdcTenantID: {ID: gpdtdcTenantID, Slug: "is-3", Name: "기술기획", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
require.NotContains(t, items[0].UpdateReasons, "grade")
require.Equal(t, "수석 연구원", items[0].BaronGrade)
}
func TestCompareWorksmobileUsersMarksConcurrentTenantGradeChangeNeedsUpdate(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
companyID := "tenant-saman"
primaryTenantID := "tenant-saman-leaf"
gpdtdcID := "tenant-gpdtdc"
gpdtdcTenantID := "tenant-is-3"
user := domain.User{
ID: "user-concurrent-grade",
Email: "concurrent-grade@samaneng.com",
Name: "Concurrent Grade User",
TenantID: &primaryTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": primaryTenantID,
"isPrimary": true,
"grade": "팀장",
},
map[string]any{
"tenantId": gpdtdcTenantID,
"grade": "책임",
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-concurrent-grade",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
LevelName: "팀장",
PrimaryOrgUnitID: "externalKey:" + primaryTenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
LevelName: "팀장",
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + primaryTenantID, Primary: true}},
},
{
DomainID: 1003,
Email: user.Email,
Primary: false,
LevelName: "선임",
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + gpdtdcTenantID, Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
primaryTenantID: {ID: primaryTenantID, Slug: "saman-leaf", Name: "삼안 조직", Type: domain.TenantTypeOrganization, ParentID: &companyID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
gpdtdcTenantID: {ID: gpdtdcTenantID, Slug: "is-3", Name: "기술기획", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
require.Contains(t, items[0].UpdateReasons, "grade")
require.NotContains(t, items[0].UpdateReasons, "organization")
require.Equal(t, "책임 연구원", items[0].BaronGrade)
require.Equal(t, "선임", items[0].WorksmobileLevelName)
}
func TestCompareWorksmobileUsersMarksSecondaryManagerChangeNeedsUpdate(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-saman"
primaryTenantID := "tenant-primary"
secondaryTenantID := "tenant-secondary"
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,
},
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + primaryTenantID,
Primary: true,
IsManager: &remotePrimaryManager,
},
{
OrgUnitID: "externalKey:" + secondaryTenantID,
Primary: false,
IsManager: &remoteSecondaryManager,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
primaryTenantID: {ID: primaryTenantID, Name: "Primary", Type: domain.TenantTypeOrganization, ParentID: &rootID},
secondaryTenantID: {ID: secondaryTenantID, Name: "Secondary", Type: domain.TenantTypeOrganization, ParentID: &rootID},
},
)
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 TestCompareWorksmobileUsersIgnoresPrimaryPriorityWhenMembershipsMatch(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
companyID := "tenant-saman"
orgChartTenantID := "tenant-rnd-saman"
gpdtdcID := "tenant-gpdtdc"
gpdtdcTenantID := "tenant-gpdtdc-leaf"
user := domain.User{
ID: "user-dual-membership",
Email: "dual-membership@samaneng.com",
Name: "Dual Membership User",
TenantID: &gpdtdcTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": orgChartTenantID,
"isPrimary": false,
},
map[string]any{
"tenantId": gpdtdcTenantID,
"isPrimary": true,
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-dual-membership",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "externalKey:" + orgChartTenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + orgChartTenantID, Primary: true}},
},
{
DomainID: 1003,
Email: user.Email,
Primary: false,
OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:" + gpdtdcTenantID, Primary: true}},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
orgChartTenantID: {ID: orgChartTenantID, Slug: "rnd-saman", Name: "삼안기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &companyID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompany, ParentID: &rootID},
gpdtdcTenantID: {ID: gpdtdcTenantID, Slug: "people-growth", Name: "인재성장", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
require.NotContains(t, items[0].UpdateReasons, "organization")
}
func TestCompareWorksmobileUsersIgnoresBaronMembershipSuperset(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-root"
companyID := "tenant-saman"
primaryTenantID := "tenant-primary"
secondaryTenantID := "tenant-secondary"
user := domain.User{
ID: "user-baron-membership-superset",
Email: "membership-superset@samaneng.com",
Name: "Membership Superset User",
TenantID: &primaryTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": primaryTenantID,
"isPrimary": true,
},
map[string]any{
"tenantId": secondaryTenantID,
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-membership-superset",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1001,
PrimaryOrgUnitID: "externalKey:" + primaryTenantID,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + primaryTenantID,
Primary: true,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
primaryTenantID: {ID: primaryTenantID, Slug: "primary", Name: "Primary", Type: domain.TenantTypeOrganization, ParentID: &companyID},
secondaryTenantID: {ID: secondaryTenantID, Slug: "secondary", Name: "Secondary", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
require.NotContains(t, items[0].UpdateReasons, "organization")
}
func TestCompareWorksmobileUsersIgnoresOrganizationEmailWhenMembershipMatches(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-root"
companyID := "tenant-saman"
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-org-email",
Email: "org-email@samaneng.com",
Name: "Org Email User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{"tenantId": tenantID},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-org-email",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + tenantID,
Primary: true,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Slug: "leaf", Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestWorksmobileUsersFromIdentityMirrorIncludesAdditionalAppointmentMembership(t *testing.T) {
tenantID := "tenant-gpdtdc-leaf"
identity := KratosIdentity{
ID: "64d4a839-ee04-4c47-b7b3-4ac6428c56b1",
Traits: map[string]any{
"email": "researcher@samaneng.com",
"name": "Researcher User",
"additionalAppointments": []any{
map[string]any{
"tenantId": tenantID,
"isPrimary": true,
"grade": "책임",
},
},
},
}
users := worksmobileUsersFromIdentityMirror([]KratosIdentity{identity}, []string{tenantID})
require.Len(t, users, 1)
require.Equal(t, identity.ID, users[0].ID)
require.NotNil(t, users[0].TenantID)
require.Equal(t, tenantID, *users[0].TenantID)
require.Equal(t, "책임", worksmobileUserGrade(users[0]))
}
func TestCompareWorksmobileUsersUsesRemoteUserDomainWhenOrganizationDomainIsMissing(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-root"
companyID := "tenant-saman"
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-org-domain",
Email: "org-domain@samaneng.com",
Name: "Org Domain User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{"tenantId": tenantID},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-org-domain",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1001,
Organizations: []WorksmobileUserOrganization{
{
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + tenantID,
Primary: true,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Slug: "leaf", Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestCompareWorksmobileUsersUsesRemotePrimaryOrgUnitWhenOrganizationPrimaryFlagsAreMissing(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-root"
companyID := "tenant-saman"
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-org-primary",
Email: "org-primary@samaneng.com",
Name: "Org Primary User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{"tenantId": tenantID},
},
},
}
items := compareWorksmobileUsersWithRemoteGroups(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-org-primary",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1001,
PrimaryOrgUnitID: "works-leaf",
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "works-leaf",
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Slug: "leaf", Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
[]WorksmobileRemoteGroup{{
ID: "works-leaf",
ExternalID: tenantID,
}},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestCompareWorksmobileUsersMatchesRemoteOrganizationExternalKey(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "tenant-root"
companyID := "tenant-saman"
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-org-external-key",
Email: "org-external-key@samaneng.com",
Name: "Org External Key User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{"tenantId": tenantID},
},
},
}
items := compareWorksmobileUsersWithRemoteGroups(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-org-external-key",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1001,
PrimaryOrgUnitID: "works-leaf",
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1001,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:" + tenantID,
Primary: true,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
companyID: {ID: companyID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}},
tenantID: {ID: tenantID, Slug: "leaf", Name: "Leaf", Type: domain.TenantTypeOrganization, ParentID: &companyID},
},
[]WorksmobileRemoteGroup{{
ID: "works-leaf",
ExternalID: tenantID,
}},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestCompareWorksmobileUsersDisplaysPrimaryAppointmentAsBaronPrimaryOrg(t *testing.T) {
t.Setenv("HANMAC_DOMAIN_ID", "1002")
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
hanmacCompanyID := "tenant-hanmac"
hanmacOrgID := "tenant-hanmac-org"
gpdtdcID := "tenant-gpdtdc"
gsimID := "tenant-gsim-dev"
user := domain.User{
ID: "user-gsim-primary",
Email: "gsim-primary@hanmaceng.co.kr",
Name: "GSIM Primary User",
TenantID: &hanmacOrgID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": hanmacOrgID,
"isPrimary": false,
},
map[string]any{
"tenantId": gsimID,
"isPrimary": true,
},
},
},
}
items := compareWorksmobileUsers(
[]domain.User{user},
nil,
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
hanmacCompanyID: {ID: hanmacCompanyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "hanmaceng.co.kr"}}},
hanmacOrgID: {ID: hanmacOrgID, Slug: "rnd-hanmac", Name: "한맥기술개발센터(조직도용)", Type: domain.TenantTypeOrganization, ParentID: &hanmacCompanyID},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompany, ParentID: &rootID},
gsimID: {ID: gsimID, Slug: "gsim-dev", Name: "GSIM개발", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "missing_in_worksmobile", items[0].Status)
require.Equal(t, gsimID, items[0].BaronPrimaryOrgID)
require.Equal(t, "gsim-dev", items[0].BaronPrimaryOrgSlug)
require.Equal(t, "GSIM개발", items[0].BaronPrimaryOrgName)
}
func TestCompareWorksmobileUsersMarksMissingPrimaryOrganizationNeedsUpdate(t *testing.T) {
t.Setenv("GPDTDC_DOMAIN_ID", "1003")
rootID := "tenant-root"
gpdtdcID := "tenant-gpdtdc"
peopleGrowthID := "tenant-people-growth"
user := domain.User{
ID: "user-people-growth",
Email: "people-growth@baroncs.co.kr",
Name: "People Growth User",
TenantID: &peopleGrowthID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-people-growth",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
DomainID: 1003,
PrimaryOrgUnitID: "externalKey:another-team",
Organizations: []WorksmobileUserOrganization{
{
DomainID: 1003,
Email: user.Email,
Primary: true,
OrgUnits: []WorksmobileUserOrgUnit{
{
OrgUnitID: "externalKey:another-team",
Primary: true,
},
},
},
},
}},
true,
map[string]domain.Tenant{
rootID: {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup},
gpdtdcID: {ID: gpdtdcID, Slug: "gpdtdc", Name: "총괄기획&기술개발센터", Type: domain.TenantTypeCompanyGroup, ParentID: &rootID},
peopleGrowthID: {ID: peopleGrowthID, Slug: "people-growth", Name: "인재성장", Type: domain.TenantTypeOrganization, ParentID: &gpdtdcID},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
require.Equal(t, peopleGrowthID, items[0].BaronPrimaryOrgID)
require.Equal(t, "externalKey:another-team", items[0].WorksmobilePrimaryOrgID)
}
func TestCompareWorksmobileUsersMatchesPrimaryOrganizationByWorksmobileResourceID(t *testing.T) {
peopleGrowthID := "tenant-people-growth"
user := domain.User{
ID: "user-people-growth",
Email: "people-growth@baroncs.co.kr",
Name: "People Growth User",
TenantID: &peopleGrowthID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsersWithRemoteGroups(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-people-growth",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "works-current-people-growth",
}},
true,
map[string]domain.Tenant{
peopleGrowthID: {ID: peopleGrowthID, Slug: "people-growth", Name: "인재성장", Type: domain.TenantTypeOrganization},
},
[]WorksmobileRemoteGroup{{
ID: "works-current-people-growth",
ExternalID: peopleGrowthID,
}},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestCompareWorksmobileUsersMarksStalePrimaryOrganizationResourceIDNeedsUpdate(t *testing.T) {
peopleGrowthID := "tenant-people-growth"
user := domain.User{
ID: "user-people-growth",
Email: "people-growth@baroncs.co.kr",
Name: "People Growth User",
TenantID: &peopleGrowthID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsersWithRemoteGroups(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-people-growth",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "works-deleted-people-growth",
}},
true,
map[string]domain.Tenant{
peopleGrowthID: {ID: peopleGrowthID, Slug: "people-growth", Name: "인재성장", Type: domain.TenantTypeOrganization},
},
[]WorksmobileRemoteGroup{{
ID: "works-current-people-growth",
ExternalID: peopleGrowthID,
}},
)
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)
require.Equal(t, user.Phone, items[0].BaronPhone)
require.Equal(t, "+821099998888", items[0].WorksmobilePhone)
require.Equal(t, "EMP001", items[0].BaronEmployeeNumber)
require.Equal(t, "EMP999", items[0].WorksmobileEmployeeNumber)
}
func TestCompareWorksmobileUsersMarksMalformedRemoteKoreanPhoneNeedsUpdate(t *testing.T) {
tenantID := "tenant-saman"
user := domain.User{
ID: "user-phone-canonical",
Email: "phone-canonical@samaneng.com",
Name: "Phone Canonical User",
Phone: "+821062836786",
TenantID: &tenantID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-phone-canonical",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
CellPhone: "+82+821062836786",
}},
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)
}
func TestCompareWorksmobileUsersIgnoresRemotePhoneWhenBaronPhoneIsEmpty(t *testing.T) {
tenantID := "tenant-halla"
user := domain.User{
ID: "edb8e4f6-3dfd-44d4-a8aa-87332f8b2b38",
Email: "cyhan4@hallasanup.com",
Name: "네이버웍스관리자",
TenantID: &tenantID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "fe9449d1-1671-44e4-1848-033779dddbaf",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
CellPhone: "+82 01041585840",
}},
true,
map[string]domain.Tenant{
tenantID: {ID: tenantID, Name: "한라산업개발", Type: domain.TenantTypeCompany},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", items[0].Status)
}
func TestCompareWorksmobileUsersTreatsSpacedKoreanCountryCodePhoneAsMatched(t *testing.T) {
tenantID := "tenant-saman"
user := domain.User{
ID: "user-phone-spaced",
Email: "phone-spaced@samaneng.com",
Name: "Phone Spaced User",
Phone: "+821041585840",
TenantID: &tenantID,
Status: domain.UserStatusActive,
}
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-phone-spaced",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
CellPhone: "+82 1041585840",
}},
true,
map[string]domain.Tenant{
tenantID: {ID: tenantID, Name: "삼안", Type: domain.TenantTypeCompany},
},
)
require.Len(t, items, 1)
require.Equal(t, "matched", 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, search 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
}
type fakeWorksmobileIdentityMirror struct {
status domain.IdentityCacheStatus
identities []KratosIdentity
}
func (f *fakeWorksmobileIdentityMirror) GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error) {
return f.status, nil
}
func (f *fakeWorksmobileIdentityMirror) ListIdentityMirrors(ctx context.Context) ([]KratosIdentity, error) {
return f.identities, nil
}
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 (r *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, 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...)
if len(tenantIDs) == 0 {
return nil, nil
}
allowed := make(map[string]bool, len(tenantIDs))
for _, tenantID := range tenantIDs {
allowed[tenantID] = true
}
users := make([]domain.User, 0, len(f.byTenant))
for _, user := range f.byTenant {
if user.TenantID == nil {
continue
}
if allowed[*user.TenantID] {
users = append(users, user)
}
}
return users, 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
}