1
0
forked from baron/baron-sso

feat: update worksmobile sync and restore planning

This commit is contained in:
2026-06-01 17:01:53 +09:00
parent 6574fb54b9
commit 5c8a338085
36 changed files with 3922 additions and 243 deletions

View File

@@ -4,11 +4,12 @@ import (
"baron-sso-backend/internal/domain"
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestWorksmobileSyncServiceRejectsAliasLocalPartAlreadyUsedByOtherUser(t *testing.T) {
func TestWorksmobileSyncServiceRejectsAliasEmailAlreadyUsedByOtherUser(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
@@ -36,7 +37,7 @@ func TestWorksmobileSyncServiceRejectsAliasLocalPartAlreadyUsedByOtherUser(t *te
}
existing := domain.User{
ID: "existing-user",
Email: "used@samaneng.com",
Email: "used@hanmaceng.co.kr",
Name: "Existing",
TenantID: &tenantID,
}
@@ -48,7 +49,7 @@ func TestWorksmobileSyncServiceRejectsAliasLocalPartAlreadyUsedByOtherUser(t *te
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "")
require.Nil(t, item)
require.Error(t, err)
@@ -88,7 +89,7 @@ func TestWorksmobileSyncServiceEnqueuesSuspendedUserStatusWithOrganizations(t *t
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "")
require.NoError(t, err)
require.NotNil(t, item)
@@ -101,6 +102,253 @@ func TestWorksmobileSyncServiceEnqueuesSuspendedUserStatusWithOrganizations(t *t
require.Equal(t, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
}
func TestWorksmobileSyncServiceEnqueuesUserCredentialBatchID(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "saman",
Name: "Saman",
Type: domain.TenantTypeCompany,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "batch-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, "batch-1", outboxRepo.created[0].Payload["credentialBatchId"])
require.NotEmpty(t, outboxRepo.created[0].Payload["credentialBatchCreatedAt"])
require.Equal(t, "Target", outboxRepo.created[0].Payload["displayName"])
require.Equal(t, "Saman", outboxRepo.created[0].Payload["primaryLeafOrgName"])
}
func TestWorksmobileSyncServiceEnqueuesUserPasswordResetCredentialBatch(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
tenantID := "saman-leaf"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
tenant := domain.Tenant{
ID: tenantID,
Slug: "people-growth",
Name: "인재성장",
Type: domain.TenantTypeOrganization,
ParentID: &rootID,
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
}
target := domain.User{
ID: "target-user",
Email: "target@samaneng.com",
Name: "Target",
Status: domain.UserStatusActive,
TenantID: &tenantID,
}
outboxRepo := &fakeWorksmobileOutboxRepo{}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
outboxRepo,
nil,
)
item, err := service.EnqueueUserPasswordReset(context.Background(), rootID, target.ID, "reset-batch-1")
require.NoError(t, err)
require.NotNil(t, item)
require.Len(t, outboxRepo.created, 1)
require.Equal(t, domain.WorksmobileActionPasswordReset, outboxRepo.created[0].Action)
require.Equal(t, "reset-batch-1", outboxRepo.created[0].Payload["credentialBatchId"])
require.Equal(t, "worksmobile_password_reset", outboxRepo.created[0].Payload["credentialOperation"])
require.NotEmpty(t, outboxRepo.created[0].Payload["credentialBatchCreatedAt"])
require.Equal(t, "target@samaneng.com", outboxRepo.created[0].Payload["loginEmail"])
require.Equal(t, "Target", outboxRepo.created[0].Payload["displayName"])
require.Equal(t, "인재성장", outboxRepo.created[0].Payload["primaryLeafOrgName"])
require.NotEmpty(t, outboxRepo.created[0].Payload["initialPassword"])
}
func TestWorksmobileSyncServiceFiltersInitialPasswordsByCredentialBatchID(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
outboxRepo := &fakeWorksmobileOutboxRepo{
credentialBatchJobs: []domain.WorksmobileOutbox{
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "batch-user@samaneng.com",
"displayName": "Batch User",
"primaryLeafOrgName": "인재성장",
"initialPassword": "BatchPass1!",
"credentialBatchId": "batch-1",
},
},
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "other-user@samaneng.com",
"initialPassword": "OtherPass1!",
"credentialBatchId": "batch-2",
},
},
{
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "legacy-user@samaneng.com",
"initialPassword": "LegacyPass1!",
},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
credentials, err := service.ListInitialPasswordCredentials(context.Background(), rootID, "batch-1")
require.NoError(t, err)
require.Equal(t, []WorksmobileInitialPasswordCredential{
{
Email: "batch-user@samaneng.com",
Name: "Batch User",
PrimaryLeafOrgName: "인재성장",
InitialPassword: "BatchPass1!",
Status: domain.WorksmobileOutboxStatusProcessed,
},
}, credentials)
}
func TestWorksmobileSyncServiceDeletesCredentialBatchPasswordsButKeepsHistory(t *testing.T) {
rootID := "root-tenant"
root := domain.Tenant{
ID: rootID,
Slug: HanmacFamilyTenantSlug,
Name: "Hanmac Family",
}
outboxRepo := &fakeWorksmobileOutboxRepo{
credentialBatchJobs: []domain.WorksmobileOutbox{
{
ID: "job-1",
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusProcessed,
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "batch-user@samaneng.com",
"initialPassword": "BatchPass1!",
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"request": map[string]any{"passwordConfig": map[string]any{"password": "BatchPass1!"}},
},
},
{
ID: "job-2",
ResourceID: "failed-user",
ResourceType: domain.WorksmobileResourceUser,
Status: domain.WorksmobileOutboxStatusFailed,
RetryCount: 2,
LastError: "worksmobile api failed",
Payload: domain.JSONMap{
"tenantRootId": rootID,
"loginEmail": "failed-user@samaneng.com",
"initialPassword": "FailedPass1!",
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"request": map[string]any{"passwordConfig": map[string]any{"password": "FailedPass1!"}},
},
},
},
}
service := NewWorksmobileSyncService(
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root}, list: []domain.Tenant{root}},
&fakeWorksmobileUserRepo{},
outboxRepo,
nil,
)
before, err := service.ListCredentialBatches(context.Background(), rootID)
require.NoError(t, err)
require.Len(t, before, 1)
require.True(t, before[0].HasPasswords)
require.Equal(t, 1, before[0].FailedCount)
require.Len(t, before[0].Failures, 1)
require.Equal(t, "failed-user", before[0].Failures[0].UserID)
require.Equal(t, "failed-user@samaneng.com", before[0].Failures[0].Email)
require.Equal(t, "worksmobile api failed", before[0].Failures[0].LastError)
after, err := service.DeleteCredentialBatchPasswords(context.Background(), rootID, "batch-1")
require.NoError(t, err)
require.Equal(t, "batch-1", after.BatchID)
require.False(t, after.HasPasswords)
require.Equal(t, 2, after.UserCount)
require.NotEmpty(t, after.DeletedAt)
require.Len(t, outboxRepo.payloadUpdates, 2)
require.Empty(t, stringValue(outboxRepo.payloadUpdates[0]["initialPassword"]))
require.Empty(t, stringValue(outboxRepo.payloadUpdates[1]["initialPassword"]))
request := outboxRepo.payloadUpdates[0]["request"].(map[string]any)
passwordConfig := request["passwordConfig"].(map[string]any)
require.Empty(t, stringValue(passwordConfig["password"]))
}
func TestAggregateWorksmobileCredentialBatchesUsesCredentialBatchCreatedAt(t *testing.T) {
oldCreatedAt := time.Date(2026, 5, 29, 1, 4, 15, 0, time.UTC)
batchCreatedAt := time.Date(2026, 6, 1, 7, 20, 0, 0, time.UTC)
batches := aggregateWorksmobileCredentialBatches([]domain.WorksmobileOutbox{
{
ID: "job-1",
CreatedAt: oldCreatedAt,
UpdatedAt: batchCreatedAt.Add(time.Minute),
Status: domain.WorksmobileOutboxStatusPending,
Payload: domain.JSONMap{
"credentialBatchId": "batch-1",
"credentialOperation": "worksmobile_user_sync",
"credentialBatchCreatedAt": batchCreatedAt.Format(time.RFC3339),
},
},
})
require.Len(t, batches, 1)
require.Equal(t, batchCreatedAt, batches[0].CreatedAt)
}
func TestWorksmobileSyncServiceDeprovisionsArchivedUser(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "1001")
rootID := "root-tenant"
@@ -133,7 +381,7 @@ func TestWorksmobileSyncServiceDeprovisionsArchivedUser(t *testing.T) {
nil,
)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID)
item, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "")
require.NoError(t, err)
require.NotNil(t, item)
@@ -1139,6 +1387,95 @@ func TestWorksmobileSyncServiceBackfillDryRunSkipsArchivedUsers(t *testing.T) {
require.Equal(t, 1, outboxRepo.created[0].Payload["userCount"])
}
func TestCompareWorksmobileUsersMarksManagerChangeNeedsUpdate(t *testing.T) {
tenantID := "tenant-leaf"
user := domain.User{
ID: "user-manager",
Email: "manager@samaneng.com",
Name: "Manager User",
TenantID: &tenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": tenantID,
"isPrimary": true,
"isManager": true,
},
},
},
}
remoteManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-manager",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "externalKey:" + tenantID,
PrimaryOrgUnitIsManager: &remoteManager,
}},
true,
map[string]domain.Tenant{
tenantID: {ID: tenantID, Name: "Leaf", Type: domain.TenantTypeOrganization},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
func TestCompareWorksmobileUsersMarksSecondaryManagerChangeNeedsUpdate(t *testing.T) {
primaryTenantID := "tenant-company"
secondaryTenantID := "tenant-gpdtdc-leaf"
user := domain.User{
ID: "user-secondary-manager",
Email: "secondary-manager@samaneng.com",
Name: "Secondary Manager User",
TenantID: &secondaryTenantID,
Status: domain.UserStatusActive,
Metadata: domain.JSONMap{
"additionalAppointments": []any{
map[string]any{
"tenantId": primaryTenantID,
"isPrimary": true,
},
map[string]any{
"tenantId": secondaryTenantID,
"isPrimary": false,
"isManager": true,
},
},
},
}
remotePrimaryManager := false
remoteSecondaryManager := false
items := compareWorksmobileUsers(
[]domain.User{user},
[]WorksmobileRemoteUser{{
ID: "works-user-secondary-manager",
ExternalID: user.ID,
Email: user.Email,
DisplayName: user.Name,
PrimaryOrgUnitID: "externalKey:" + primaryTenantID,
PrimaryOrgUnitIsManager: &remotePrimaryManager,
OrgUnitManagers: map[string]*bool{
"externalKey:" + primaryTenantID: &remotePrimaryManager,
"externalKey:" + secondaryTenantID: &remoteSecondaryManager,
},
}},
true,
map[string]domain.Tenant{
primaryTenantID: {ID: primaryTenantID, Name: "Company", Type: domain.TenantTypeCompany},
secondaryTenantID: {ID: secondaryTenantID, Name: "GPDTDC Leaf", Type: domain.TenantTypeOrganization},
},
)
require.Len(t, items, 1)
require.Equal(t, "needs_update", items[0].Status)
}
type fakeWorksmobileTenantService struct {
tenants map[string]domain.Tenant
list []domain.Tenant