package service import ( "baron-sso-backend/internal/domain" "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "io" "net/http" "net/url" "os" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestWorksmobileHTTPClientCreateUserPostsDirectoryAdminPasswordPayload(t *testing.T) { transport := &captureRoundTripper{ statusCode: http.StatusCreated, body: `{}`, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", SCIMToken: "scim-token-1", HTTPClient: &http.Client{Transport: transport}, } err := client.CreateUser(context.Background(), WorksmobileUserPayload{ DomainID: 300285955, Email: "tester@samaneng.com", UserExternalKey: "user-1", UserName: WorksmobileUserName{LastName: "Tester"}, AliasEmails: []string{"tester.alias@samaneng.com", "tester.alias2@samaneng.com"}, Locale: "ko_KR", PasswordConfig: WorksmobilePasswordConfig{ PasswordCreationType: "ADMIN", Password: GenerateWorksmobileInitialPassword(), }, Organizations: []WorksmobileUserOrganization{ {DomainID: 300285955, Primary: true, OrgUnits: []WorksmobileUserOrgUnit{{OrgUnitID: "externalKey:tenant-saman"}}}, }, }) require.NoError(t, err) require.NotNil(t, transport.request) require.Equal(t, "/v1.0/users", transport.request.URL.Path) require.Equal(t, http.MethodPost, transport.request.Method) require.Equal(t, "Bearer directory-token-1", transport.request.Header.Get("Authorization")) var payload map[string]any require.NoError(t, json.Unmarshal(transport.requestBody, &payload)) require.Equal(t, "tester@samaneng.com", payload["email"]) require.Equal(t, "user-1", payload["userExternalKey"]) require.NotContains(t, payload, "privateEmail") require.Equal(t, []any{"tester.alias@samaneng.com", "tester.alias2@samaneng.com"}, payload["aliasEmails"]) passwordConfig := payload["passwordConfig"].(map[string]any) require.Equal(t, "ADMIN", passwordConfig["passwordCreationType"]) require.Len(t, passwordConfig["password"], 16) } func TestWorksmobileHTTPClientUpsertUserPatchesOnCreateConflictWithoutPasswordOrPrivateEmail(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusConflict, body: `{"code":"ALREADY_EXISTS"}`}, {statusCode: http.StatusOK, body: `{}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", HTTPClient: &http.Client{Transport: transport}, } err := client.UpsertUser(context.Background(), WorksmobileUserPayload{ DomainID: 300285955, Email: "tester@samaneng.com", UserExternalKey: "user-1", UserName: WorksmobileUserName{LastName: "Tester"}, PrivateEmail: "private@example.com", PasswordConfig: WorksmobilePasswordConfig{ PasswordCreationType: "ADMIN", Password: GenerateWorksmobileInitialPassword(), }, Organizations: []WorksmobileUserOrganization{ { DomainID: 300285955, Primary: true, OrgUnits: []WorksmobileUserOrgUnit{ {OrgUnitID: "externalKey:tenant-saman", Primary: true}, }, }, }, }) require.NoError(t, err) require.Len(t, transport.requests, 2) require.Equal(t, http.MethodPost, transport.requests[0].Method) require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path) require.Equal(t, http.MethodPatch, transport.requests[1].Method) require.Equal(t, "/v1.0/users/tester@samaneng.com", transport.requests[1].URL.Path) var patchPayload map[string]any require.NoError(t, json.Unmarshal(transport.requestBodies[1], &patchPayload)) require.NotContains(t, patchPayload, "passwordConfig") require.NotContains(t, patchPayload, "privateEmail") require.Equal(t, "tester@samaneng.com", patchPayload["email"]) 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", SCIMToken: "scim-token-1", HTTPClient: &http.Client{Transport: &captureRoundTripper{statusCode: http.StatusCreated, body: `{}`}}, } err := client.CreateUser(context.Background(), WorksmobileUserPayload{Email: "tester@samaneng.com"}) require.Error(t, err) require.Contains(t, err.Error(), "worksmobile directory token is not configured") } func TestWorksmobileHTTPClientRequestsJWTBearerAccessToken(t *testing.T) { privateKey := testRSAPrivateKeyPEM(t) transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusOK, body: `{"access_token":"directory-token-from-jwt","token_type":"Bearer","expires_in":3600}`}, {statusCode: http.StatusCreated, body: `{}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", HTTPClient: &http.Client{Transport: transport}, now: func() time.Time { return time.Unix(1710000000, 0) }, OAuthConfig: WorksmobileOAuthConfig{ ClientID: "client-id-1", ClientSecret: "client-secret-1", ServiceAccount: "service-account-1", PrivateKey: privateKey, Scope: "directory", TokenURL: "https://auth.example.test/token", }, } err := client.CreateUser(context.Background(), WorksmobileUserPayload{ DomainID: 300285955, Email: "tester@samaneng.com", UserExternalKey: "user-1", UserName: WorksmobileUserName{LastName: "Tester"}, PasswordConfig: WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"}, }) require.NoError(t, err) require.Len(t, transport.requests, 2) require.Equal(t, "https://auth.example.test/token", transport.requests[0].URL.String()) require.Equal(t, "/v1.0/users", transport.requests[1].URL.Path) require.Equal(t, "Bearer directory-token-from-jwt", transport.requests[1].Header.Get("Authorization")) form, err := url.ParseQuery(string(transport.requestBodies[0])) require.NoError(t, err) require.Equal(t, "urn:ietf:params:oauth:grant-type:jwt-bearer", form.Get("grant_type")) require.Equal(t, "client-id-1", form.Get("client_id")) require.Equal(t, "client-secret-1", form.Get("client_secret")) require.Equal(t, "directory", form.Get("scope")) parts := strings.Split(form.Get("assertion"), ".") require.Len(t, parts, 3) payloadData, err := base64.RawURLEncoding.DecodeString(parts[1]) require.NoError(t, err) var payload map[string]any require.NoError(t, json.Unmarshal(payloadData, &payload)) require.Equal(t, "client-id-1", payload["iss"]) require.Equal(t, "service-account-1", payload["sub"]) require.Equal(t, float64(1710000000), payload["iat"]) require.Equal(t, float64(1710003600), payload["exp"]) } func TestWorksmobileHTTPClientRequiresConfiguredAPIBaseURL(t *testing.T) { client := &WorksmobileHTTPClient{ DirectoryToken: "directory-token-1", HTTPClient: &http.Client{Transport: &captureRoundTripper{statusCode: http.StatusCreated, body: `{}`}}, } err := client.CreateUser(context.Background(), WorksmobileUserPayload{ DomainID: 300285955, Email: "tester@samaneng.com", UserExternalKey: "user-1", UserName: WorksmobileUserName{LastName: "Tester"}, PasswordConfig: WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"}, }) require.Error(t, err) require.Contains(t, err.Error(), "worksmobile api base url is not configured") } func TestWorksmobileHTTPClientRequiresConfiguredOAuthTokenURL(t *testing.T) { privateKey := testRSAPrivateKeyPEM(t) client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", OAuthConfig: WorksmobileOAuthConfig{ ClientID: "client-id-1", ClientSecret: "client-secret-1", ServiceAccount: "service-account-1", PrivateKey: privateKey, Scope: "directory", }, } _, _, err := client.requestDirectoryAccessToken( context.Background(), time.Unix(1710000000, 0), ) require.Error(t, err) require.Contains(t, err.Error(), "worksmobile oauth token url is not configured") } func TestWorksmobileHTTPClientListUsersUsesDirectoryAPIFirst(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "300285955") transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusOK, body: `{"users":[{"userId":"works-user-1","userExternalKey":"user-1","email":"tester@samaneng.com","userName":{"lastName":"Tester"},"organizations":[{"primary":true,"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"삼안"}]}]}],"responseMetaData":{}}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", SCIMToken: "scim-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, } users, err := client.ListUsers(context.Background()) require.NoError(t, err) require.Len(t, users, 1) require.Equal(t, "user-1", users[0].ExternalID) require.Equal(t, "tester@samaneng.com", users[0].Email) require.Equal(t, int64(300285955), users[0].DomainID) require.Equal(t, "삼안", users[0].DomainName) require.Equal(t, "works-org-1", users[0].PrimaryOrgUnitID) require.Len(t, transport.requests, 1) require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path) require.Equal(t, "300285955", transport.requests[0].URL.Query().Get("domainId")) } func TestWorksmobileHTTPClientListUsersFallsBackToSCIMWhenDirectoryFails(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusForbidden, body: `{"code":"FORBIDDEN"}`}, {statusCode: http.StatusOK, body: `{"totalResults":1,"Resources":[{"id":"scim-user-1","userName":"tester@samaneng.com","displayName":"Tester","emails":[]}]} `}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", SCIMToken: "scim-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, } users, err := client.ListUsers(context.Background()) require.NoError(t, err) require.Len(t, users, 1) require.Equal(t, "scim-user-1", users[0].ID) require.Equal(t, "tester@samaneng.com", users[0].Email) require.Equal(t, "/v1.0/users", transport.requests[0].URL.Path) require.Equal(t, "/scim/v2/Users", transport.requests[1].URL.Path) } func TestWorksmobileHTTPClientSetUserActivePatchesSCIMActiveFlag(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusOK, body: `{"totalResults":1,"Resources":[{"id":"scim-user-1","externalId":"user-1","userName":"tester@samaneng.com","active":true,"emails":[{"value":"tester@samaneng.com","primary":true}]}]}`}, {statusCode: http.StatusOK, body: `{}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", SCIMToken: "scim-token-1", HTTPClient: &http.Client{Transport: transport}, } err := client.SetUserActive(context.Background(), "tester@samaneng.com", false) require.NoError(t, err) require.Len(t, transport.requests, 2) require.Equal(t, http.MethodGet, transport.requests[0].Method) require.Equal(t, "/scim/v2/Users", transport.requests[0].URL.Path) require.Equal(t, http.MethodPatch, transport.requests[1].Method) require.Equal(t, "/scim/v2/Users/scim-user-1", transport.requests[1].URL.Path) require.Equal(t, "Bearer scim-token-1", transport.requests[1].Header.Get("Authorization")) var patchPayload map[string]any require.Len(t, transport.requestBodies, 1) require.NoError(t, json.Unmarshal(transport.requestBodies[0], &patchPayload)) operations, ok := patchPayload["Operations"].([]any) require.True(t, ok) require.Len(t, operations, 1) operation, ok := operations[0].(map[string]any) require.True(t, ok) require.Equal(t, "replace", operation["op"]) require.Equal(t, "active", operation["path"]) require.Equal(t, false, operation["value"]) } func TestWorksmobileHTTPClientListGroupsUsesDirectoryAPIFirst(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "300285955") transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitExternalKey":"tenant-1","orgUnitName":"삼안","parentOrgUnitId":"parent-1","parentOrgUnitName":"상위"}],"responseMetaData":{}}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", SCIMToken: "scim-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, } groups, err := client.ListGroups(context.Background()) require.NoError(t, err) require.Len(t, groups, 1) require.Equal(t, "tenant-1", groups[0].ExternalID) require.Equal(t, "삼안", groups[0].DisplayName) require.Equal(t, int64(300285955), groups[0].DomainID) require.Equal(t, "삼안", groups[0].DomainName) require.Equal(t, "parent-1", groups[0].ParentID) require.Equal(t, "/v1.0/orgunits", transport.requests[0].URL.Path) } func TestWorksmobileHTTPClientUpsertOrgUnitBackfillsExternalKeyByMailLocalPart(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`}, {statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"기술개발센터","email":"tech-dev-center@samaneng.com"}],"responseMetaData":{}}`}, {statusCode: http.StatusOK, body: `{}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, OrgUnitWriteDelay: -1, } err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{ DomainID: 300285955, OrgUnitName: "기술개발센터", OrgUnitExternalKey: "tenant-tech-dev-center", DisplayOrder: 0, }, "tech-dev-center") require.NoError(t, err) require.Len(t, transport.requests, 3) require.Equal(t, http.MethodPost, transport.requests[0].Method) require.Equal(t, "/v1.0/orgunits", transport.requests[0].URL.Path) require.Contains(t, string(transport.requestBodies[0]), `"displayOrder":1`) require.Equal(t, http.MethodGet, transport.requests[1].Method) require.Equal(t, "/v1.0/orgunits", transport.requests[1].URL.Path) require.Equal(t, http.MethodPatch, transport.requests[2].Method) require.Equal(t, "/v1.0/orgunits/works-org-1", transport.requests[2].URL.Path) require.Contains(t, string(transport.requestBodies[1]), `"orgUnitExternalKey":"tenant-tech-dev-center"`) } func TestWorksmobileHTTPClientUpsertOrgUnitDoesNotBackfillExternalKeyByName(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`}, {statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitName":"기술개발센터","email":"legacy-tech@samaneng.com"}],"responseMetaData":{}}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, OrgUnitWriteDelay: -1, } err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{ DomainID: 300285955, OrgUnitName: "기술개발센터", OrgUnitExternalKey: "tenant-tech-dev-center", DisplayOrder: 1, }, "tech-dev-center") require.Error(t, err) require.Contains(t, err.Error(), "external key match not found") require.Len(t, transport.requests, 2) require.Equal(t, http.MethodGet, transport.requests[1].Method) } func TestWorksmobileHTTPClientUpsertOrgUnitTreatsExistingExternalKeyConflictAsSuccess(t *testing.T) { transport := &captureRoundTripper{ responses: []captureResponse{ {statusCode: http.StatusConflict, body: `{"code":"CONFLICT"}`}, {statusCode: http.StatusOK, body: `{"orgUnits":[{"orgUnitId":"works-org-1","orgUnitExternalKey":"tenant-tech-dev-center","orgUnitName":"기술개발센터"}],"responseMetaData":{}}`}, }, } client := &WorksmobileHTTPClient{ BaseURL: "https://works.example.test", DirectoryToken: "directory-token-1", DomainIDs: []int64{300285955}, HTTPClient: &http.Client{Transport: transport}, } err := client.UpsertOrgUnit(context.Background(), WorksmobileOrgUnitPayload{ DomainID: 300285955, OrgUnitName: "기술개발센터", OrgUnitExternalKey: "tenant-tech-dev-center", }, "") require.NoError(t, err) require.Len(t, transport.requests, 3) require.Equal(t, http.MethodPost, transport.requests[0].Method) require.Equal(t, http.MethodGet, transport.requests[1].Method) require.Equal(t, http.MethodPatch, transport.requests[2].Method) require.Contains(t, string(transport.requestBodies[1]), `"orgUnitExternalKey":"tenant-tech-dev-center"`) } func TestWorksmobileLiveJWTTokenExchange(t *testing.T) { if os.Getenv("WORKSMOBILE_LIVE_JWT_TOKEN_EXCHANGE") != "1" { t.Skip("live Worksmobile token exchange is disabled") } client := newWorksmobileLiveClient() token, err := client.directoryAccessToken(context.Background()) require.NoError(t, err) require.NotEmpty(t, token) } func TestWorksmobileRelayWorkerProcessesUserCreateAndMarksProcessed(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: "tester@samaneng.com", UserExternalKey: "user-1", 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.processingIDs) require.Equal(t, []string{"job-1"}, repo.processedIDs) 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{ { ID: "job-1", ResourceType: domain.WorksmobileResourceUser, ResourceID: "user-1", Action: domain.WorksmobileActionSuspend, Status: domain.WorksmobileOutboxStatusPending, Payload: worksmobileUserOutboxPayload("root-1", WorksmobileUserPayload{ Email: "tester@samaneng.com", UserExternalKey: "user-1", }), }, }, } client := &fakeWorksmobileDirectoryClient{} worker := NewWorksmobileRelayWorker(repo, client) err := worker.ProcessOnce(context.Background()) require.NoError(t, err) require.Equal(t, []string{"job-1"}, repo.processingIDs) require.Equal(t, []string{"job-1"}, repo.processedIDs) require.Equal(t, []string{"tester@samaneng.com"}, client.suspendedUsers) } func TestWorksmobileRelayWorkerProcessesActiveUserUpsertAndReactivates(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: "tester@samaneng.com", UserExternalKey: "user-1", }, domain.UserStatusActive), }, }, } 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, "tester@samaneng.com", client.createdUsers[0].Email) require.Equal(t, []string{"tester@samaneng.com"}, client.activeUsers) } func TestWorksmobileRelayWorkerProcessesOrgUnitDeleteAndMarksProcessed(t *testing.T) { repo := &fakeWorksmobileOutboxRepo{ ready: []domain.WorksmobileOutbox{ { ID: "job-1", ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: "works-org-1", Action: domain.WorksmobileActionDelete, Status: domain.WorksmobileOutboxStatusPending, Payload: domain.JSONMap{"worksmobileId": "works-org-1"}, }, }, } 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, []string{"works-org-1"}, client.deletedOrgUnits) } func TestWorksmobileRelayWorkerProcessesOrgUnitParentsBeforeChildren(t *testing.T) { repo := &fakeWorksmobileOutboxRepo{ ready: []domain.WorksmobileOutbox{ { ID: "job-child", ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: "child-tenant", Action: domain.WorksmobileActionUpsert, Status: domain.WorksmobileOutboxStatusPending, Payload: domain.JSONMap{ "request": map[string]any{ "domainId": 300293726, "orgUnitExternalKey": "child-tenant", "orgUnitName": "child", "parentOrgUnitId": "externalKey:parent-tenant", }, }, }, { ID: "job-parent", ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: "parent-tenant", Action: domain.WorksmobileActionUpsert, Status: domain.WorksmobileOutboxStatusPending, Payload: domain.JSONMap{ "request": map[string]any{ "domainId": 300293726, "orgUnitExternalKey": "parent-tenant", "orgUnitName": "parent", }, }, }, }, } client := &fakeWorksmobileDirectoryClient{} worker := NewWorksmobileRelayWorker(repo, client) err := worker.ProcessOnce(context.Background()) require.NoError(t, err) require.Equal(t, []string{"job-parent", "job-child"}, repo.processingIDs) require.Equal(t, []string{"parent-tenant", "child-tenant"}, []string{ client.createdOrgUnits[0].OrgUnitExternalKey, client.createdOrgUnits[1].OrgUnitExternalKey, }) } func TestWorksmobileRelayWorkerSkipsDispatchWhenJobClaimFails(t *testing.T) { repo := &fakeWorksmobileOutboxRepo{ markProcessingClaims: map[string]bool{"job-claimed-by-other-worker": false}, ready: []domain.WorksmobileOutbox{ { ID: "job-claimed-by-other-worker", ResourceType: domain.WorksmobileResourceOrgUnit, ResourceID: "org-1", Action: domain.WorksmobileActionUpsert, Status: domain.WorksmobileOutboxStatusPending, Payload: domain.JSONMap{ "request": map[string]any{ "domainId": 300293726, "orgUnitExternalKey": "org-1", "orgUnitName": "org", }, }, }, }, } client := &fakeWorksmobileDirectoryClient{} worker := NewWorksmobileRelayWorker(repo, client) err := worker.ProcessOnce(context.Background()) require.NoError(t, err) require.Empty(t, repo.processingIDs) require.Empty(t, repo.processedIDs) require.Empty(t, client.createdOrgUnits) } func TestRedactWorksmobileOutboxPayloadsRemovesInitialPasswordFromOverview(t *testing.T) { jobs := []domain.WorksmobileOutbox{ { ID: "job-1", Payload: domain.JSONMap{ "loginEmail": "tester@samaneng.com", "initialPassword": "Aa1!Aa1!Aa1!Aa1!", }, }, } redacted := redactWorksmobileOutboxPayloads(jobs) require.Equal(t, "tester@samaneng.com", redacted[0].Payload["loginEmail"]) require.NotContains(t, redacted[0].Payload, "initialPassword") } func TestCompareWorksmobileUsersHidesMatchedByDefault(t *testing.T) { localUsers := []domain.User{ {ID: "user-1", Email: "matched@samaneng.com", Name: "Matched"}, {ID: "user-2", Email: "missing@samaneng.com", Name: "Missing"}, } remoteUsers := []WorksmobileRemoteUser{ {ID: "works-1", ExternalID: "user-1", Email: "matched@samaneng.com", DisplayName: "Matched"}, } diffOnly := compareWorksmobileUsers(localUsers, remoteUsers, false, nil) all := compareWorksmobileUsers(localUsers, remoteUsers, true, nil) require.Len(t, diffOnly, 1) require.Equal(t, "missing_in_worksmobile", diffOnly[0].Status) require.Len(t, all, 2) require.Equal(t, "matched", all[0].Status) } func TestCompareWorksmobileUsersIncludesBaronAndWorksPrimaryOrg(t *testing.T) { tenantID := "tenant-primary" localUsers := []domain.User{ {ID: "user-1", Email: "matched@samaneng.com", Name: "Matched", TenantID: &tenantID}, } localTenants := map[string]domain.Tenant{ tenantID: {ID: tenantID, Name: "기술기획", Slug: "tech-planning"}, } remoteUsers := []WorksmobileRemoteUser{ { ID: "works-1", ExternalID: "user-1", Email: "matched@samaneng.com", DisplayName: "Matched", DomainID: 300285955, DomainName: "삼안", PrimaryOrgUnitID: "works-org-1", PrimaryOrgUnitName: "WORKS 기술기획", }, } items := compareWorksmobileUsers(localUsers, remoteUsers, true, localTenants) require.Len(t, items, 1) require.Equal(t, tenantID, items[0].BaronPrimaryOrgID) require.Equal(t, "기술기획", items[0].BaronPrimaryOrgName) require.Equal(t, int64(300285955), items[0].WorksmobileDomainID) require.Equal(t, "삼안", items[0].WorksmobileDomainName) require.Equal(t, "works-org-1", items[0].WorksmobilePrimaryOrgID) require.Equal(t, "WORKS 기술기획", items[0].WorksmobilePrimaryOrgName) } func TestCompareWorksmobileUsersMarksEmailMatchWithoutExternalIDNeedsUpdate(t *testing.T) { localUsers := []domain.User{ {ID: "user-1", Email: "tester@samaneng.com", Name: "Tester"}, } remoteUsers := []WorksmobileRemoteUser{ {ID: "works-1", ExternalID: "", Email: "tester@samaneng.com", DisplayName: "Tester"}, } diffOnly := compareWorksmobileUsers(localUsers, remoteUsers, false, nil) all := compareWorksmobileUsers(localUsers, remoteUsers, true, nil) require.Len(t, diffOnly, 1) require.Equal(t, "needs_update", diffOnly[0].Status) require.Len(t, all, 1) 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"}, } items := compareWorksmobileUsers(nil, remoteUsers, true, nil) require.Len(t, items, 1) require.Equal(t, "missing_external_key", items[0].Status) require.Equal(t, "works-1", items[0].WorksmobileID) require.Equal(t, "works-only@samaneng.com", items[0].WorksmobileEmail) } func TestCompareWorksmobileGroupsIncludesBaronAndWorksParentOrg(t *testing.T) { parentID := "tenant-parent" childID := "tenant-child" localTenants := []domain.Tenant{ {ID: parentID, Slug: "tech-hq", Name: "기술본부", Type: domain.TenantTypeOrganization}, {ID: childID, Slug: "tech-planning", Name: "기술기획", Type: domain.TenantTypeOrganization, ParentID: &parentID}, } remoteGroups := []WorksmobileRemoteGroup{ { ID: "works-parent", ExternalID: parentID, DisplayName: "WORKS 기술본부", DomainID: 300286337, DomainName: "총괄기획&기술개발센터", }, { ID: "works-child", ExternalID: childID, DisplayName: "WORKS 기술기획", DomainID: 300286337, DomainName: "총괄기획&기술개발센터", ParentID: "works-parent", ParentName: "WORKS 기술본부", }, } items := compareWorksmobileGroups(localTenants, remoteGroups, true) require.Len(t, items, 2) require.Equal(t, "tech-planning", items[1].BaronSlug) require.Equal(t, parentID, items[1].BaronParentID) require.Equal(t, "tech-hq", items[1].BaronParentSlug) require.Equal(t, "기술본부", items[1].BaronParentName) require.Equal(t, int64(300286337), items[1].WorksmobileDomainID) require.Equal(t, "총괄기획&기술개발센터", items[1].WorksmobileDomainName) require.Equal(t, "works-parent", items[1].WorksmobileParentID) require.Equal(t, "WORKS 기술본부", items[1].WorksmobileParentName) } func TestCompareWorksmobileGroupsDoesNotMatchDomainCompanyByDomainID(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") parentID := "root-tenant" localTenants := []domain.Tenant{ { ID: "company-saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &parentID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}, }, } remoteGroups := []WorksmobileRemoteGroup{ { ID: "works-org-1", DisplayName: "WORKS 전용 조직", DomainID: 1001, DomainName: "삼안", }, } items := compareWorksmobileGroups(localTenants, remoteGroups, true) require.Len(t, items, 1) require.Empty(t, items[0].BaronID) require.Equal(t, "missing_external_key", items[0].Status) } func TestCompareWorksmobileGroupsIncludesWorksOnlyRowsWithoutExternalIDWhenIncludingMatched(t *testing.T) { remoteGroups := []WorksmobileRemoteGroup{ {ID: "works-group-1", ExternalID: "", DisplayName: "WORKS 전용 조직"}, } items := compareWorksmobileGroups(nil, remoteGroups, true) require.Len(t, items, 1) require.Equal(t, "missing_external_key", items[0].Status) require.Equal(t, "works-group-1", items[0].WorksmobileID) require.Equal(t, "WORKS 전용 조직", items[0].WorksmobileName) } func TestCompareWorksmobileGroupsDoesNotMatchBySlugLocalPartWhenExternalIDMissing(t *testing.T) { localTenants := []domain.Tenant{ { ID: "tenant-tech-dev-center", Slug: "tech-dev-center", Name: "기술개발센터", Type: domain.TenantTypeOrganization, }, } remoteGroups := []WorksmobileRemoteGroup{ { ID: "works-org-1", DisplayName: "기술개발센터", Email: "tech-dev-center@samaneng.com", MailLocalPart: "tech-dev-center", }, } diffOnly := compareWorksmobileGroups(localTenants, remoteGroups, false) all := compareWorksmobileGroups(localTenants, remoteGroups, true) require.Len(t, diffOnly, 2) require.Equal(t, "missing_in_worksmobile", diffOnly[0].Status) require.Equal(t, "tenant-tech-dev-center", diffOnly[0].BaronID) require.Equal(t, "missing_external_key", diffOnly[1].Status) require.Equal(t, "works-org-1", diffOnly[1].WorksmobileID) require.Len(t, all, 2) require.Equal(t, "missing_in_worksmobile", all[0].Status) require.Equal(t, "tenant-tech-dev-center", all[0].BaronID) require.Equal(t, "missing_external_key", all[1].Status) require.Equal(t, "works-org-1", all[1].WorksmobileID) require.Empty(t, all[1].ExternalKey) } func TestCompareWorksmobileGroupsDoesNotMatchByNameWhenExternalIDAndSlugAreMissing(t *testing.T) { localTenants := []domain.Tenant{ { ID: "tenant-tech-dev-center", Slug: "tech-dev-center", Name: "기술개발센터", Type: domain.TenantTypeOrganization, }, } remoteGroups := []WorksmobileRemoteGroup{ { ID: "works-org-1", DisplayName: "기술개발센터", }, } items := compareWorksmobileGroups(localTenants, remoteGroups, false) require.Len(t, items, 2) require.Equal(t, "missing_in_worksmobile", items[0].Status) require.Equal(t, "tenant-tech-dev-center", items[0].BaronID) require.Equal(t, "missing_external_key", items[1].Status) require.Equal(t, "works-org-1", items[1].WorksmobileID) } func TestCompareWorksmobileGroupsListsExternalKeyMissingRowsAsDeleteCandidatesAcrossDomains(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") t.Setenv("HANMAC_DOMAIN_ID", "1002") t.Setenv("GPDTDC_DOMAIN_ID", "1003") t.Setenv("BARONGROUP_DOMAIN_ID", "1004") rootID := "root-tenant" samanID := "company-saman" hanmacID := "company-hanmac" localTenants := []domain.Tenant{ {ID: rootID, Slug: HanmacFamilyTenantSlug, Name: "한맥가족", Type: domain.TenantTypeCompanyGroup}, {ID: samanID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "samaneng.com"}}}, {ID: hanmacID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID, Domains: []domain.TenantDomain{{Domain: "hanmaceng.co.kr"}}}, {ID: "tenant-saman-planning", Slug: "planning", Name: "기획팀", Type: domain.TenantTypeOrganization, ParentID: &samanID}, {ID: "tenant-hanmac-planning", Slug: "planning", Name: "기획팀", Type: domain.TenantTypeOrganization, ParentID: &hanmacID}, } remoteGroups := []WorksmobileRemoteGroup{ {ID: "works-saman-planning", DomainID: 1001, DisplayName: "기획팀", MailLocalPart: "planning"}, {ID: "works-hanmac-planning", DomainID: 1002, DisplayName: "기획팀", MailLocalPart: "planning"}, } items := compareWorksmobileGroups(localTenants, remoteGroups, false) require.Len(t, items, 4) require.Equal(t, "tenant-saman-planning", items[0].BaronID) require.Equal(t, "missing_in_worksmobile", items[0].Status) require.Equal(t, "tenant-hanmac-planning", items[1].BaronID) require.Equal(t, "missing_in_worksmobile", items[1].Status) require.Equal(t, "works-saman-planning", items[2].WorksmobileID) require.Equal(t, "missing_external_key", items[2].Status) require.Equal(t, "works-hanmac-planning", items[3].WorksmobileID) require.Equal(t, "missing_external_key", items[3].Status) } func TestParseWorksmobileRemoteUserUsesUserNameEmailWhenEmailsAreEmpty(t *testing.T) { user := parseWorksmobileRemoteUser(map[string]any{ "id": "works-1", "userName": "tester@samaneng.com", "displayName": "Tester", "emails": []any{}, }) require.Equal(t, "tester@samaneng.com", user.UserName) require.Equal(t, "tester@samaneng.com", user.Email) } func TestParseWorksmobileRemoteResourcesExtractsOrgFields(t *testing.T) { user := parseWorksmobileRemoteUser(map[string]any{ "id": "works-user", "externalId": "user-1", "organizations": []any{ map[string]any{ "primary": true, "orgUnitId": "works-org-1", "orgUnitName": "WORKS 기술기획", }, }, }) group := parseWorksmobileRemoteGroup(map[string]any{ "id": "works-group", "externalId": "group-1", "parent": map[string]any{ "id": "works-parent", "displayName": "WORKS 기술본부", }, }) require.Equal(t, "works-org-1", user.PrimaryOrgUnitID) require.Equal(t, "WORKS 기술기획", user.PrimaryOrgUnitName) require.Equal(t, "works-parent", group.ParentID) require.Equal(t, "WORKS 기술본부", group.ParentName) } func TestParseWorksmobileDirectoryUserIncludesFullNameLevelAndOrgRole(t *testing.T) { user := parseWorksmobileDirectoryUser(map[string]any{ "userId": "works-user", "email": "tester@samaneng.com", "userName": map[string]any{ "lastName": "홍", "firstName": "길동", }, "levelId": "level-1", "levelName": "책임", "task": "기술검토", "organizations": []any{ map[string]any{ "primary": true, "orgUnits": []any{ map[string]any{ "orgUnitId": "works-org-1", "orgUnitName": "기술기획", "positionId": "position-1", "positionName": "팀장", "isManager": true, }, }, }, }, }) require.Equal(t, "홍길동", user.DisplayName) require.Equal(t, "level-1", user.LevelID) require.Equal(t, "책임", user.LevelName) require.Equal(t, "기술검토", user.Task) require.Equal(t, "works-org-1", user.PrimaryOrgUnitID) require.Equal(t, "기술기획", user.PrimaryOrgUnitName) require.Equal(t, "position-1", user.PrimaryOrgUnitPositionID) 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", "cellPhone": "010-1234-5678", "employeeNumber": "EMP001", "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"]) require.Equal(t, "010-1234-5678", user.CellPhone) require.Equal(t, "EMP001", user.EmployeeNumber) require.Equal(t, []WorksmobileUserOrganization{ { Primary: true, OrgUnits: []WorksmobileUserOrgUnit{ {OrgUnitID: "externalKey:primary-org", Primary: true, IsManager: boolPtr(false)}, {OrgUnitID: "externalKey:secondary-org", Primary: false, IsManager: boolPtr(true)}, }, }, }, user.Organizations) } func TestParseWorksmobileDirectoryGroupExtractsMailLocalPart(t *testing.T) { group := parseWorksmobileDirectoryGroup(map[string]any{ "orgUnitId": "works-org-1", "orgUnitName": "기술개발센터", "email": "tech-dev-center@samaneng.com", }) require.Equal(t, "tech-dev-center@samaneng.com", group.Email) require.Equal(t, "tech-dev-center", group.MailLocalPart) } func boolPtr(value bool) *bool { return &value } type fakeWorksmobileOutboxRepo struct { recent []domain.WorksmobileOutbox ready []domain.WorksmobileOutbox created []domain.WorksmobileOutbox credentialBatchJobs []domain.WorksmobileOutbox payloadUpdates []domain.JSONMap deletedPendingTenantRootID string deletedPendingCount int markProcessingClaims map[string]bool processingIDs []string processedIDs []string failedIDs []string } func (f *fakeWorksmobileOutboxRepo) Create(ctx context.Context, item *domain.WorksmobileOutbox) error { f.created = append(f.created, *item) return nil } func (f *fakeWorksmobileOutboxRepo) ListRecent(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) { 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) DeletePendingByTenantRoot(ctx context.Context, tenantRootID string) (int64, error) { f.deletedPendingTenantRootID = tenantRootID return int64(f.deletedPendingCount), nil } func (f *fakeWorksmobileOutboxRepo) ListReady(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) { return f.ready, nil } func (f *fakeWorksmobileOutboxRepo) FindByID(ctx context.Context, id string) (*domain.WorksmobileOutbox, error) { return nil, nil } func (f *fakeWorksmobileOutboxRepo) MarkRetry(ctx context.Context, id string) error { return nil } func (f *fakeWorksmobileOutboxRepo) MarkProcessing(ctx context.Context, id string) (bool, error) { if f.markProcessingClaims != nil && !f.markProcessingClaims[id] { return false, nil } f.processingIDs = append(f.processingIDs, id) return true, nil } func (f *fakeWorksmobileOutboxRepo) MarkProcessed(ctx context.Context, id string) error { f.processedIDs = append(f.processedIDs, id) return nil } func (f *fakeWorksmobileOutboxRepo) MarkFailed(ctx context.Context, id string, message string, nextAttemptAt time.Time) error { f.failedIDs = append(f.failedIDs, id) return nil } type fakeWorksmobileDirectoryClient struct { createdOrgUnits []WorksmobileOrgUnitPayload deletedOrgUnits []string createdUsers []WorksmobileUserPayload deletedUsers []string activeUsers []string suspendedUsers []string aliasEmails []string passwordResets []string users []WorksmobileRemoteUser orgUnitMatchKeys []string groups []WorksmobileRemoteGroup } type captureRoundTripper struct { request *http.Request requestBody []byte statusCode int body string responses []captureResponse requests []*http.Request requestBodies [][]byte } type captureResponse struct { statusCode int body string } func (t *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { t.request = req t.requests = append(t.requests, req) if req.Body != nil { data, err := io.ReadAll(req.Body) if err != nil { return nil, err } t.requestBody = data t.requestBodies = append(t.requestBodies, data) } statusCode := t.statusCode body := t.body if len(t.responses) > 0 { response := t.responses[0] t.responses = t.responses[1:] statusCode = response.statusCode body = response.body } if statusCode == 0 { statusCode = http.StatusOK } return &http.Response{ StatusCode: statusCode, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: req, }, nil } func testRSAPrivateKeyPEM(t *testing.T) string { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) data := x509.MarshalPKCS1PrivateKey(key) return string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: data})) } func getenvDefault(key string, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func newWorksmobileLiveClient() *WorksmobileHTTPClient { client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{ ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"), ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"), PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"), Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"), TokenURL: os.Getenv("WORKS_ADMIN_OAUTH_TOKEN_URL"), }) client.BaseURL = os.Getenv("WORKS_ADMIN_API_BASE_URL") return client } func (f *fakeWorksmobileDirectoryClient) CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error { f.createdOrgUnits = append(f.createdOrgUnits, payload) return nil } func (f *fakeWorksmobileDirectoryClient) UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error { f.createdOrgUnits = append(f.createdOrgUnits, payload) f.orgUnitMatchKeys = append(f.orgUnitMatchKeys, matchLocalPart) return nil } func (f *fakeWorksmobileDirectoryClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error { f.deletedOrgUnits = append(f.deletedOrgUnits, orgUnitID) return nil } func (f *fakeWorksmobileDirectoryClient) CreateUser(ctx context.Context, payload WorksmobileUserPayload) error { f.createdUsers = append(f.createdUsers, payload) return nil } func (f *fakeWorksmobileDirectoryClient) UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error { f.createdUsers = append(f.createdUsers, 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 } func (f *fakeWorksmobileDirectoryClient) SetUserActive(ctx context.Context, userID string, active bool) error { if active { f.activeUsers = append(f.activeUsers, userID) } else { f.suspendedUsers = append(f.suspendedUsers, userID) } return nil } func (f *fakeWorksmobileDirectoryClient) ListUsers(ctx context.Context) ([]WorksmobileRemoteUser, error) { return f.users, nil } func (f *fakeWorksmobileDirectoryClient) ListGroups(ctx context.Context) ([]WorksmobileRemoteGroup, error) { return f.groups, nil }