forked from baron/baron-sso
kratos SSOT 재설계
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -199,6 +200,39 @@ func (s *RedisService) FlushIdentityCache(ctx context.Context) (domain.IdentityC
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RedisService) ListIdentityMirrors(ctx context.Context) ([]KratosIdentity, error) {
|
||||
if s == nil || s.Client == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
keys, err := s.identityCacheKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identities := make([]KratosIdentity, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if key == "identity:mirror:state" || !strings.HasPrefix(key, "identity:mirror:") {
|
||||
continue
|
||||
}
|
||||
raw, err := s.Client.Get(ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var identity KratosIdentity
|
||||
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(identity.ID) == "" {
|
||||
continue
|
||||
}
|
||||
identities = append(identities, identity)
|
||||
}
|
||||
return identities, nil
|
||||
}
|
||||
|
||||
func (s *RedisService) countIdentityCacheKeys(ctx context.Context) (int64, error) {
|
||||
keys, err := s.identityCacheKeys(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -434,6 +434,9 @@ func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) e
|
||||
if userID == "" {
|
||||
return fmt.Errorf("worksmobile user id is required")
|
||||
}
|
||||
if c.directoryAuthConfigured() && strings.Contains(userID, "@") {
|
||||
return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/users/"+url.PathEscape(userID), nil)
|
||||
}
|
||||
remote, err := c.FindUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -450,6 +453,14 @@ func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) e
|
||||
return c.sendJSON(ctx, http.MethodDelete, "/scim/v2/Users/"+url.PathEscape(remote.ID), nil)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) ForceDeleteUser(ctx context.Context, userID string) error {
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID == "" {
|
||||
return fmt.Errorf("worksmobile user id is required")
|
||||
}
|
||||
return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/users/"+url.PathEscape(userID)+"/forcedelete", nil)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) SetUserActive(ctx context.Context, userID string, active bool) error {
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID == "" {
|
||||
@@ -465,7 +476,18 @@ func (c *WorksmobileHTTPClient) SetUserActive(ctx context.Context, userID string
|
||||
if remote == nil {
|
||||
return nil
|
||||
}
|
||||
return c.sendJSON(ctx, http.MethodPatch, "/scim/v2/Users/"+url.PathEscape(remote.ID), map[string]any{
|
||||
return c.SetSCIMUserActiveByID(ctx, remote.ID, active)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) SetSCIMUserActiveByID(ctx context.Context, scimID string, active bool) error {
|
||||
scimID = strings.TrimSpace(scimID)
|
||||
if scimID == "" {
|
||||
return fmt.Errorf("worksmobile scim user id is required")
|
||||
}
|
||||
if strings.TrimSpace(c.SCIMToken) == "" {
|
||||
return fmt.Errorf("worksmobile scim token is not configured")
|
||||
}
|
||||
return c.sendJSON(ctx, http.MethodPatch, "/scim/v2/Users/"+url.PathEscape(scimID), map[string]any{
|
||||
"schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
|
||||
"Operations": []map[string]any{
|
||||
{
|
||||
@@ -926,6 +948,7 @@ type WorksmobileRemoteUser struct {
|
||||
PrimaryOrgUnitIsManager *bool `json:"primaryOrgUnitIsManager,omitempty"`
|
||||
OrgUnitManagers map[string]*bool `json:"orgUnitManagers,omitempty"`
|
||||
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
|
||||
AccountStatus string `json:"accountStatus,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
IsAwaiting bool `json:"isAwaiting"`
|
||||
IsPending bool `json:"isPending"`
|
||||
@@ -1010,12 +1033,21 @@ func worksmobileSCIMPreferredLanguage(locale string) string {
|
||||
}
|
||||
|
||||
func parseWorksmobileRemoteUser(resource map[string]any) WorksmobileRemoteUser {
|
||||
active := boolFromMap(resource, "active")
|
||||
user := WorksmobileRemoteUser{
|
||||
ID: stringFromMap(resource, "id"),
|
||||
ExternalID: stringFromMap(resource, "externalId"),
|
||||
UserName: stringFromMap(resource, "userName"),
|
||||
DisplayName: stringFromMap(resource, "displayName"),
|
||||
Active: boolFromMap(resource, "active"),
|
||||
AccountStatus: normalizeWorksmobileAccountStatus(
|
||||
firstStringFromMap(resource, "accountStatus", "status", "userStatus"),
|
||||
active,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Active: active,
|
||||
}
|
||||
if emails, ok := resource["emails"].([]any); ok {
|
||||
for _, raw := range emails {
|
||||
@@ -1077,6 +1109,14 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
|
||||
user.IsPending = boolFromMap(resource, "isPending")
|
||||
user.IsSuspended = boolFromMap(resource, "isSuspended")
|
||||
user.IsDeleted = boolFromMap(resource, "isDeleted")
|
||||
user.AccountStatus = normalizeWorksmobileAccountStatus(
|
||||
firstStringFromMap(resource, "accountStatus", "status", "userStatus", "loginStatus"),
|
||||
user.Active,
|
||||
user.IsAwaiting,
|
||||
user.IsPending,
|
||||
user.IsSuspended,
|
||||
user.IsDeleted,
|
||||
)
|
||||
primaryOrgUnit := parseWorksmobilePrimaryOrgUnitDetail(resource)
|
||||
user.PrimaryOrgUnitID = primaryOrgUnit.ID
|
||||
user.PrimaryOrgUnitName = primaryOrgUnit.Name
|
||||
@@ -1088,6 +1128,37 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
|
||||
return user
|
||||
}
|
||||
|
||||
func normalizeWorksmobileAccountStatus(raw string, active bool, awaiting bool, pending bool, suspended bool, deleted bool) string {
|
||||
status := strings.ToLower(strings.TrimSpace(raw))
|
||||
status = strings.ReplaceAll(status, "-", "_")
|
||||
status = strings.ReplaceAll(status, " ", "_")
|
||||
switch status {
|
||||
case "deleted", "delete", "removed":
|
||||
return "deleted"
|
||||
case "suspended", "suspend", "blocked", "disabled":
|
||||
return "suspended"
|
||||
case "invited", "invite", "awaiting", "pending", "waiting", "not_activated", "unactivated":
|
||||
return "invited"
|
||||
case "inactive", "deactivated", "false":
|
||||
return "inactive"
|
||||
case "active", "enabled", "true":
|
||||
return "active"
|
||||
}
|
||||
if deleted {
|
||||
return "deleted"
|
||||
}
|
||||
if suspended {
|
||||
return "suspended"
|
||||
}
|
||||
if awaiting || pending {
|
||||
return "invited"
|
||||
}
|
||||
if !active {
|
||||
return "inactive"
|
||||
}
|
||||
return "active"
|
||||
}
|
||||
|
||||
func parseWorksmobileDirectoryGroup(resource map[string]any) WorksmobileRemoteGroup {
|
||||
email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
|
||||
return WorksmobileRemoteGroup{
|
||||
|
||||
@@ -64,6 +64,26 @@ func TestWorksmobileHTTPClientCreateUserPostsDirectoryAdminPasswordPayload(t *te
|
||||
require.Len(t, passwordConfig["password"], 16)
|
||||
}
|
||||
|
||||
func TestWorksmobileHTTPClientDeleteUserUsesDirectDirectoryDeleteForEmail(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.DeleteUser(context.Background(), "target@samaneng.com")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, transport.requests, 1)
|
||||
require.Equal(t, http.MethodDelete, transport.requests[0].Method)
|
||||
require.Equal(t, "/v1.0/users/target@samaneng.com", transport.requests[0].URL.Path)
|
||||
require.Equal(t, "Bearer directory-token-1", transport.requests[0].Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestNewWorksmobileUserPatchPayloadNormalizesMalformedKoreanCellPhone(t *testing.T) {
|
||||
payload := NewWorksmobileUserPatchPayload(WorksmobileUserPayload{
|
||||
DomainID: 1001,
|
||||
@@ -975,6 +995,27 @@ func TestCompareWorksmobileUsersIncludesBaronAndWorksPrimaryOrg(t *testing.T) {
|
||||
require.Equal(t, "WORKS 기술기획", items[0].WorksmobilePrimaryOrgName)
|
||||
}
|
||||
|
||||
func TestCompareWorksmobileUsersIncludesWorksAccountStatus(t *testing.T) {
|
||||
localUsers := []domain.User{
|
||||
{ID: "user-1", Email: "suspended@samaneng.com", Name: "Suspended"},
|
||||
}
|
||||
remoteUsers := []WorksmobileRemoteUser{
|
||||
{
|
||||
ID: "works-1",
|
||||
ExternalID: "user-1",
|
||||
Email: "suspended@samaneng.com",
|
||||
DisplayName: "Suspended",
|
||||
Active: false,
|
||||
IsSuspended: true,
|
||||
},
|
||||
}
|
||||
|
||||
items := compareWorksmobileUsers(localUsers, remoteUsers, true, nil)
|
||||
|
||||
require.Len(t, items, 1)
|
||||
require.Equal(t, "suspended", items[0].WorksmobileAccountStatus)
|
||||
}
|
||||
|
||||
func TestCompareWorksmobileUsersMarksEmailMatchWithoutExternalIDNeedsUpdate(t *testing.T) {
|
||||
localUsers := []domain.User{
|
||||
{ID: "user-1", Email: "tester@samaneng.com", Name: "Tester"},
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -106,6 +108,8 @@ type WorksmobileComparisonItem struct {
|
||||
BaronSlug string `json:"baronSlug,omitempty"`
|
||||
BaronName string `json:"baronName,omitempty"`
|
||||
BaronEmail string `json:"baronEmail,omitempty"`
|
||||
BaronPhone string `json:"baronPhone,omitempty"`
|
||||
BaronEmployeeNumber string `json:"baronEmployeeNumber,omitempty"`
|
||||
BaronPrimaryOrgID string `json:"baronPrimaryOrgId,omitempty"`
|
||||
BaronPrimaryOrgSlug string `json:"baronPrimaryOrgSlug,omitempty"`
|
||||
BaronPrimaryOrgName string `json:"baronPrimaryOrgName,omitempty"`
|
||||
@@ -116,6 +120,9 @@ type WorksmobileComparisonItem struct {
|
||||
ExternalKey string `json:"externalKey,omitempty"`
|
||||
WorksmobileName string `json:"worksmobileName,omitempty"`
|
||||
WorksmobileEmail string `json:"worksmobileEmail,omitempty"`
|
||||
WorksmobilePhone string `json:"worksmobilePhone,omitempty"`
|
||||
WorksmobileEmployeeNumber string `json:"worksmobileEmployeeNumber,omitempty"`
|
||||
WorksmobileAccountStatus string `json:"worksmobileAccountStatus,omitempty"`
|
||||
WorksmobileLevelID string `json:"worksmobileLevelId,omitempty"`
|
||||
WorksmobileLevelName string `json:"worksmobileLevelName,omitempty"`
|
||||
WorksmobileTask string `json:"worksmobileTask,omitempty"`
|
||||
@@ -571,7 +578,7 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
|
||||
ResourceType: domain.WorksmobileResourceUser,
|
||||
ResourceID: user.ID,
|
||||
Action: action,
|
||||
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
|
||||
DedupeKey: worksmobileUserSyncDedupeKey(action, user.ID),
|
||||
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
|
||||
}
|
||||
item.Payload["displayName"] = strings.TrimSpace(user.Name)
|
||||
@@ -587,6 +594,10 @@ func (s *worksmobileSyncService) EnqueueUserSync(ctx context.Context, tenantID,
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func worksmobileUserSyncDedupeKey(action, userID string) string {
|
||||
return "user:" + strings.ToLower(action) + ":" + userID + ":" + uuid.NewString()
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) {
|
||||
root, err := s.hanmacRoot(ctx, tenantID)
|
||||
if err != nil {
|
||||
@@ -880,7 +891,7 @@ func (s *worksmobileSyncService) EnqueueUserUpsertIfInScope(ctx context.Context,
|
||||
ResourceType: domain.WorksmobileResourceUser,
|
||||
ResourceID: user.ID,
|
||||
Action: action,
|
||||
DedupeKey: "user:" + strings.ToLower(action) + ":" + user.ID,
|
||||
DedupeKey: worksmobileUserSyncDedupeKey(action, user.ID),
|
||||
Payload: worksmobileUserOutboxPayload(root.ID, payload, user.Status),
|
||||
})
|
||||
}
|
||||
@@ -1461,6 +1472,8 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
BaronID: user.ID,
|
||||
BaronName: user.Name,
|
||||
BaronEmail: user.Email,
|
||||
BaronPhone: user.Phone,
|
||||
BaronEmployeeNumber: metadataEmployeeNumber(user.Metadata),
|
||||
BaronPrimaryOrgID: worksmobileUserPrimaryOrgID(user),
|
||||
BaronPrimaryOrgSlug: worksmobileUserPrimaryOrgSlug(user, localTenants),
|
||||
BaronPrimaryOrgName: worksmobileUserPrimaryOrgName(user, localTenants),
|
||||
@@ -1483,6 +1496,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
item.ExternalKey = remote.ExternalID
|
||||
item.WorksmobileName = remote.DisplayName
|
||||
item.WorksmobileEmail = remote.Email
|
||||
item.WorksmobilePhone = remote.CellPhone
|
||||
item.WorksmobileEmployeeNumber = remote.EmployeeNumber
|
||||
item.WorksmobileAccountStatus = worksmobileRemoteAccountStatus(remote)
|
||||
item.WorksmobileLevelID = remote.LevelID
|
||||
item.WorksmobileLevelName = remote.LevelName
|
||||
item.WorksmobileTask = remote.Task
|
||||
@@ -1511,6 +1527,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
ExternalKey: remote.ExternalID,
|
||||
WorksmobileName: remote.DisplayName,
|
||||
WorksmobileEmail: remote.Email,
|
||||
WorksmobilePhone: remote.CellPhone,
|
||||
WorksmobileEmployeeNumber: remote.EmployeeNumber,
|
||||
WorksmobileAccountStatus: worksmobileRemoteAccountStatus(remote),
|
||||
WorksmobileLevelID: remote.LevelID,
|
||||
WorksmobileLevelName: remote.LevelName,
|
||||
WorksmobileTask: remote.Task,
|
||||
@@ -1532,6 +1551,9 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
ExternalKey: remote.ExternalID,
|
||||
WorksmobileName: remote.DisplayName,
|
||||
WorksmobileEmail: remote.Email,
|
||||
WorksmobilePhone: remote.CellPhone,
|
||||
WorksmobileEmployeeNumber: remote.EmployeeNumber,
|
||||
WorksmobileAccountStatus: worksmobileRemoteAccountStatus(remote),
|
||||
WorksmobileLevelID: remote.LevelID,
|
||||
WorksmobileLevelName: remote.LevelName,
|
||||
WorksmobileTask: remote.Task,
|
||||
@@ -1549,6 +1571,17 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
|
||||
return result
|
||||
}
|
||||
|
||||
func worksmobileRemoteAccountStatus(remote WorksmobileRemoteUser) string {
|
||||
return normalizeWorksmobileAccountStatus(
|
||||
remote.AccountStatus,
|
||||
remote.Active,
|
||||
remote.IsAwaiting,
|
||||
remote.IsPending,
|
||||
remote.IsSuspended,
|
||||
remote.IsDeleted,
|
||||
)
|
||||
}
|
||||
|
||||
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
|
||||
if strings.TrimSpace(remote.ExternalID) != strings.TrimSpace(user.ID) {
|
||||
return true
|
||||
|
||||
@@ -204,6 +204,88 @@ func TestWorksmobileSyncServiceDoesNotAutoGenerateInitialPassword(t *testing.T)
|
||||
require.Empty(t, request.PasswordConfig.Password)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceCreatesDistinctUserSyncHistoryJobs(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "root-tenant"
|
||||
tenantID := "saman-tenant"
|
||||
root := domain.Tenant{
|
||||
ID: rootID,
|
||||
Slug: HanmacFamilyTenantSlug,
|
||||
Name: "Hanmac Family",
|
||||
}
|
||||
tenant := domain.Tenant{
|
||||
ID: tenantID,
|
||||
Slug: "saman",
|
||||
Name: "Saman",
|
||||
Type: domain.TenantTypeCompany,
|
||||
ParentID: &rootID,
|
||||
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
|
||||
}
|
||||
target := domain.User{
|
||||
ID: "target-user",
|
||||
Email: "target@samaneng.com",
|
||||
Name: "Target",
|
||||
Status: domain.UserStatusActive,
|
||||
TenantID: &tenantID,
|
||||
}
|
||||
outboxRepo := &fakeWorksmobileOutboxRepo{}
|
||||
service := NewWorksmobileSyncService(
|
||||
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
|
||||
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
|
||||
outboxRepo,
|
||||
nil,
|
||||
)
|
||||
|
||||
first, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
|
||||
require.NoError(t, err)
|
||||
second, err := service.EnqueueUserSync(context.Background(), rootID, target.ID, "", "")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, first)
|
||||
require.NotNil(t, second)
|
||||
require.Len(t, outboxRepo.created, 2)
|
||||
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceCreatesDistinctAutomaticUserSyncHistoryJobs(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "root-tenant"
|
||||
tenantID := "saman-tenant"
|
||||
root := domain.Tenant{
|
||||
ID: rootID,
|
||||
Slug: HanmacFamilyTenantSlug,
|
||||
Name: "Hanmac Family",
|
||||
}
|
||||
tenant := domain.Tenant{
|
||||
ID: tenantID,
|
||||
Slug: "saman",
|
||||
Name: "Saman",
|
||||
Type: domain.TenantTypeCompany,
|
||||
ParentID: &rootID,
|
||||
Domains: []domain.TenantDomain{{Domain: "samaneng.com"}},
|
||||
}
|
||||
target := domain.User{
|
||||
ID: "target-user",
|
||||
Email: "target@samaneng.com",
|
||||
Name: "Target",
|
||||
Status: domain.UserStatusActive,
|
||||
TenantID: &tenantID,
|
||||
}
|
||||
outboxRepo := &fakeWorksmobileOutboxRepo{}
|
||||
service := NewWorksmobileSyncService(
|
||||
&fakeWorksmobileTenantService{tenants: map[string]domain.Tenant{rootID: root, tenantID: tenant}, list: []domain.Tenant{root, tenant}},
|
||||
&fakeWorksmobileUserRepo{byID: map[string]domain.User{target.ID: target}, byTenant: []domain.User{target}},
|
||||
outboxRepo,
|
||||
nil,
|
||||
)
|
||||
|
||||
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
|
||||
require.NoError(t, service.EnqueueUserUpsertIfInScope(context.Background(), target))
|
||||
|
||||
require.Len(t, outboxRepo.created, 2)
|
||||
require.NotEqual(t, outboxRepo.created[0].DedupeKey, outboxRepo.created[1].DedupeKey)
|
||||
}
|
||||
|
||||
func TestWorksmobileSyncServiceEnqueuesUserPasswordResetCredentialBatch(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "root-tenant"
|
||||
@@ -2050,6 +2132,10 @@ func TestCompareWorksmobileUsersMarksPhoneAndEmployeeNumberChangesNeedsUpdate(t
|
||||
|
||||
require.Len(t, items, 1)
|
||||
require.Equal(t, "needs_update", items[0].Status)
|
||||
require.Equal(t, user.Phone, items[0].BaronPhone)
|
||||
require.Equal(t, "+821099998888", items[0].WorksmobilePhone)
|
||||
require.Equal(t, "EMP001", items[0].BaronEmployeeNumber)
|
||||
require.Equal(t, "EMP999", items[0].WorksmobileEmployeeNumber)
|
||||
}
|
||||
|
||||
func TestCompareWorksmobileUsersMarksMalformedRemoteKoreanPhoneNeedsUpdate(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user