forked from baron/baron-sso
1400 lines
49 KiB
Go
1400 lines
49 KiB
Go
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
|
|
}
|