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

@@ -113,6 +113,50 @@ func TestWorksmobileHTTPClientUpsertUserPatchesOnCreateConflictWithoutPasswordOr
require.Equal(t, "user-1", patchPayload["userExternalKey"])
}
func TestWorksmobileHTTPClientAddUserAliasEmailPostsDirectoryAliasEndpoint(t *testing.T) {
transport := &captureRoundTripper{
statusCode: http.StatusCreated,
body: `{}`,
}
client := &WorksmobileHTTPClient{
BaseURL: "https://works.example.test",
DirectoryToken: "directory-token-1",
HTTPClient: &http.Client{Transport: transport},
}
err := client.AddUserAliasEmail(context.Background(), "ypshim@samaneng.com", "ypshim@hanmaceng.co.kr")
require.NoError(t, err)
require.NotNil(t, transport.request)
require.Equal(t, http.MethodPost, transport.request.Method)
require.Equal(t, "/v1.0/users/ypshim@samaneng.com/alias-emails/ypshim@hanmaceng.co.kr", transport.request.URL.Path)
require.Equal(t, "Bearer directory-token-1", transport.request.Header.Get("Authorization"))
}
func TestWorksmobileHTTPClientResetUserPasswordPatchesPasswordConfig(t *testing.T) {
transport := &captureRoundTripper{
statusCode: http.StatusOK,
body: `{}`,
}
client := &WorksmobileHTTPClient{
BaseURL: "https://works.example.test",
DirectoryToken: "directory-token-1",
HTTPClient: &http.Client{Transport: transport},
}
err := client.ResetUserPassword(context.Background(), "target@samaneng.com", "Aa1!Aa1!Aa1!Aa1!")
require.NoError(t, err)
require.NotNil(t, transport.request)
require.Equal(t, http.MethodPatch, transport.request.Method)
require.Equal(t, "/v1.0/users/target@samaneng.com", transport.request.URL.Path)
var payload map[string]any
require.NoError(t, json.Unmarshal(transport.requestBody, &payload))
passwordConfig := payload["passwordConfig"].(map[string]any)
require.Equal(t, "ADMIN", passwordConfig["passwordCreationType"])
require.Equal(t, "Aa1!Aa1!Aa1!Aa1!", passwordConfig["password"])
}
func TestWorksmobileHTTPClientCreateUserRequiresDirectoryToken(t *testing.T) {
client := &WorksmobileHTTPClient{
BaseURL: "https://works.example.test",
@@ -472,6 +516,71 @@ func TestWorksmobileRelayWorkerProcessesUserCreateAndMarksProcessed(t *testing.T
require.Equal(t, "tester@samaneng.com", client.createdUsers[0].Email)
}
func TestWorksmobileRelayWorkerRegistersAliasEmailsAfterUserUpsert(t *testing.T) {
repo := &fakeWorksmobileOutboxRepo{
ready: []domain.WorksmobileOutbox{
{
ID: "job-1",
ResourceType: domain.WorksmobileResourceUser,
ResourceID: "user-1",
Action: domain.WorksmobileActionUpsert,
Status: domain.WorksmobileOutboxStatusPending,
Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{
Email: "ypshim@samaneng.com",
UserExternalKey: "user-1",
AliasEmails: []string{"ypshim@hanmaceng.co.kr"},
PasswordConfig: WorksmobilePasswordConfig{
PasswordCreationType: "ADMIN",
Password: "Aa1!Aa1!Aa1!Aa1!",
},
}),
},
},
}
client := &fakeWorksmobileDirectoryClient{}
worker := NewWorksmobileRelayWorker(repo, client)
err := worker.ProcessOnce(context.Background())
require.NoError(t, err)
require.Equal(t, []string{"job-1"}, repo.processedIDs)
require.Equal(t, "ypshim@samaneng.com", client.createdUsers[0].Email)
require.Empty(t, client.createdUsers[0].AliasEmails)
require.Equal(t, []string{"ypshim@samaneng.com:ypshim@hanmaceng.co.kr"}, client.aliasEmails)
}
func TestWorksmobileRelayWorkerProcessesUserPasswordResetAndMarksProcessed(t *testing.T) {
repo := &fakeWorksmobileOutboxRepo{
ready: []domain.WorksmobileOutbox{
{
ID: "job-reset",
ResourceType: domain.WorksmobileResourceUser,
ResourceID: "user-1",
Action: domain.WorksmobileActionPasswordReset,
Status: domain.WorksmobileOutboxStatusPending,
Payload: domain.JSONMap{
"loginEmail": "target@samaneng.com",
"request": map[string]any{
"email": "target@samaneng.com",
"passwordConfig": map[string]any{
"passwordCreationType": "ADMIN",
"password": "Aa1!Aa1!Aa1!Aa1!",
},
},
},
},
},
}
client := &fakeWorksmobileDirectoryClient{}
worker := NewWorksmobileRelayWorker(repo, client)
err := worker.ProcessOnce(context.Background())
require.NoError(t, err)
require.Equal(t, []string{"job-reset"}, repo.processedIDs)
require.Equal(t, []string{"target@samaneng.com:Aa1!Aa1!Aa1!Aa1!"}, client.passwordResets)
}
func TestWorksmobileRelayWorkerProcessesUserSuspendAndMarksProcessed(t *testing.T) {
repo := &fakeWorksmobileOutboxRepo{
ready: []domain.WorksmobileOutbox{
@@ -615,7 +724,7 @@ func TestCompareWorksmobileUsersIncludesBaronAndWorksPrimaryOrg(t *testing.T) {
require.Equal(t, "WORKS 기술기획", items[0].WorksmobilePrimaryOrgName)
}
func TestCompareWorksmobileUsersMatchesByEmailWhenDirectoryAPIOmitsExternalID(t *testing.T) {
func TestCompareWorksmobileUsersMarksEmailMatchWithoutExternalIDNeedsUpdate(t *testing.T) {
localUsers := []domain.User{
{ID: "user-1", Email: "tester@samaneng.com", Name: "Tester"},
}
@@ -626,13 +735,37 @@ func TestCompareWorksmobileUsersMatchesByEmailWhenDirectoryAPIOmitsExternalID(t
diffOnly := compareWorksmobileUsers(localUsers, remoteUsers, false, nil)
all := compareWorksmobileUsers(localUsers, remoteUsers, true, nil)
require.Empty(t, diffOnly)
require.Len(t, diffOnly, 1)
require.Equal(t, "needs_update", diffOnly[0].Status)
require.Len(t, all, 1)
require.Equal(t, "matched", all[0].Status)
require.Equal(t, "needs_update", all[0].Status)
require.Equal(t, "works-1", all[0].WorksmobileID)
require.Empty(t, all[0].ExternalKey)
}
func TestCompareWorksmobileUsersIncludesRecentFailedJobForMissingUser(t *testing.T) {
localUsers := []domain.User{
{ID: "user-1", Email: "missing@samaneng.com", Name: "Missing"},
}
jobSummaries := map[string]worksmobileUserJobSummary{
"user-1": {
Status: domain.WorksmobileOutboxStatusFailed,
RetryCount: 3,
LastError: "worksmobile api failed",
LastAttemptAt: "2026-06-01T05:00:00Z",
},
}
items := compareWorksmobileUsers(localUsers, nil, false, nil, jobSummaries)
require.Len(t, items, 1)
require.Equal(t, "missing_in_worksmobile", items[0].Status)
require.Equal(t, domain.WorksmobileOutboxStatusFailed, items[0].WorksmobileJobStatus)
require.Equal(t, 3, items[0].WorksmobileJobRetryCount)
require.Equal(t, "worksmobile api failed", items[0].WorksmobileLastError)
require.Equal(t, "2026-06-01T05:00:00Z", items[0].WorksmobileLastAttemptAt)
}
func TestCompareWorksmobileUsersIncludesWorksOnlyRowsWithoutExternalIDWhenIncludingMatched(t *testing.T) {
remoteUsers := []WorksmobileRemoteUser{
{ID: "works-1", ExternalID: "", Email: "works-only@samaneng.com", DisplayName: "Works Only"},
@@ -894,6 +1027,41 @@ func TestParseWorksmobileDirectoryUserIncludesFullNameLevelAndOrgRole(t *testing
require.Equal(t, "팀장", user.PrimaryOrgUnitPositionName)
require.NotNil(t, user.PrimaryOrgUnitIsManager)
require.True(t, *user.PrimaryOrgUnitIsManager)
require.NotNil(t, user.OrgUnitManagers["works-org-1"])
require.True(t, *user.OrgUnitManagers["works-org-1"])
}
func TestParseWorksmobileDirectoryUserIncludesAllOrgUnitManagerFlags(t *testing.T) {
user := parseWorksmobileDirectoryUser(map[string]any{
"userId": "works-user",
"email": "tester@samaneng.com",
"userName": map[string]any{
"lastName": "홍길동",
},
"organizations": []any{
map[string]any{
"primary": true,
"orgUnits": []any{
map[string]any{
"orgUnitId": "externalKey:primary-org",
"primary": true,
"isManager": false,
},
map[string]any{
"orgUnitId": "externalKey:secondary-org",
"primary": false,
"isManager": true,
},
},
},
},
})
require.Len(t, user.OrgUnitManagers, 2)
require.NotNil(t, user.OrgUnitManagers["externalKey:primary-org"])
require.False(t, *user.OrgUnitManagers["externalKey:primary-org"])
require.NotNil(t, user.OrgUnitManagers["externalKey:secondary-org"])
require.True(t, *user.OrgUnitManagers["externalKey:secondary-org"])
}
func TestParseWorksmobileDirectoryGroupExtractsMailLocalPart(t *testing.T) {
@@ -908,11 +1076,14 @@ func TestParseWorksmobileDirectoryGroupExtractsMailLocalPart(t *testing.T) {
}
type fakeWorksmobileOutboxRepo struct {
ready []domain.WorksmobileOutbox
created []domain.WorksmobileOutbox
processingIDs []string
processedIDs []string
failedIDs []string
recent []domain.WorksmobileOutbox
ready []domain.WorksmobileOutbox
created []domain.WorksmobileOutbox
credentialBatchJobs []domain.WorksmobileOutbox
payloadUpdates []domain.JSONMap
processingIDs []string
processedIDs []string
failedIDs []string
}
func (f *fakeWorksmobileOutboxRepo) Create(ctx context.Context, item *domain.WorksmobileOutbox) error {
@@ -921,7 +1092,31 @@ func (f *fakeWorksmobileOutboxRepo) Create(ctx context.Context, item *domain.Wor
}
func (f *fakeWorksmobileOutboxRepo) ListRecent(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) {
return nil, nil
return f.recent, nil
}
func (f *fakeWorksmobileOutboxRepo) ListCredentialBatchJobs(ctx context.Context, tenantRootID, credentialBatchID string) ([]domain.WorksmobileOutbox, error) {
rows := make([]domain.WorksmobileOutbox, 0)
for _, row := range f.credentialBatchJobs {
if stringValue(row.Payload["tenantRootId"]) != tenantRootID {
continue
}
if credentialBatchID != "" && stringValue(row.Payload["credentialBatchId"]) != credentialBatchID {
continue
}
rows = append(rows, row)
}
return rows, nil
}
func (f *fakeWorksmobileOutboxRepo) UpdatePayload(ctx context.Context, id string, payload domain.JSONMap) error {
f.payloadUpdates = append(f.payloadUpdates, payload)
for i := range f.credentialBatchJobs {
if f.credentialBatchJobs[i].ID == id {
f.credentialBatchJobs[i].Payload = payload
}
}
return nil
}
func (f *fakeWorksmobileOutboxRepo) ListReady(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) {
@@ -958,6 +1153,8 @@ type fakeWorksmobileDirectoryClient struct {
deletedUsers []string
activeUsers []string
suspendedUsers []string
aliasEmails []string
passwordResets []string
users []WorksmobileRemoteUser
orgUnitMatchKeys []string
groups []WorksmobileRemoteGroup
@@ -1062,6 +1259,16 @@ func (f *fakeWorksmobileDirectoryClient) UpsertUser(ctx context.Context, payload
return nil
}
func (f *fakeWorksmobileDirectoryClient) AddUserAliasEmail(ctx context.Context, userID string, email string) error {
f.aliasEmails = append(f.aliasEmails, userID+":"+email)
return nil
}
func (f *fakeWorksmobileDirectoryClient) ResetUserPassword(ctx context.Context, userID string, password string) error {
f.passwordResets = append(f.passwordResets, userID+":"+password)
return nil
}
func (f *fakeWorksmobileDirectoryClient) DeleteUser(ctx context.Context, userID string) error {
f.deletedUsers = append(f.deletedUsers, userID)
return nil