package main import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "context" "encoding/csv" "errors" "strings" "testing" ) func TestResolveCreateSuperAdminConfigUsesEnvDefaults(t *testing.T) { t.Setenv("ADMIN_EMAIL", "admin@example.com") t.Setenv("ADMIN_PASSWORD", "Password!123") t.Setenv("ADMIN_NAME", "Env Admin") config, err := resolveCreateSuperAdminConfig([]string{}) if err != nil { t.Fatalf("resolveCreateSuperAdminConfig returned error: %v", err) } if config.Email != "admin@example.com" { t.Fatalf("email = %q", config.Email) } if config.Password != "Password!123" { t.Fatal("password was not read from ADMIN_PASSWORD") } if config.Name != "Env Admin" { t.Fatalf("name = %q", config.Name) } } func TestResolveCreateSuperAdminConfigAllowsFlagOverrides(t *testing.T) { t.Setenv("ADMIN_EMAIL", "admin@example.com") t.Setenv("ADMIN_PASSWORD", "Password!123") t.Setenv("ADMIN_NAME", "Env Admin") config, err := resolveCreateSuperAdminConfig([]string{ "--email", "flag@example.com", "--password", "FlagPassword!123", "--name", "Flag Admin", "--update-password", }) if err != nil { t.Fatalf("resolveCreateSuperAdminConfig returned error: %v", err) } if config.Email != "flag@example.com" { t.Fatalf("email = %q", config.Email) } if config.Password != "FlagPassword!123" { t.Fatal("password flag was not used") } if config.Name != "Flag Admin" { t.Fatalf("name = %q", config.Name) } if !config.UpdatePassword { t.Fatal("update password flag was not set") } } func TestResolveCreateSuperAdminConfigRequiresEmailAndPassword(t *testing.T) { t.Setenv("ADMIN_EMAIL", "") t.Setenv("ADMIN_PASSWORD", "") if _, err := resolveCreateSuperAdminConfig([]string{}); err == nil { t.Fatal("expected error") } } func TestResolveClearOrphanUserTenantMembershipsConfig(t *testing.T) { config, err := resolveClearOrphanUserTenantMembershipsConfig([]string{"--dry-run"}) if err != nil { t.Fatalf("resolveClearOrphanUserTenantMembershipsConfig returned error: %v", err) } if !config.DryRun { t.Fatal("dry-run flag was not set") } } func TestAuditWorksmobileDuplicatePhoneCountryCodesReportsAndFixes(t *testing.T) { client := &fakeWorksmobilePhoneAuditClient{ users: []service.WorksmobileRemoteUser{ { ID: "works-user-1", ExternalID: "baron-user-1", Email: "one@example.com", DisplayName: "One", CellPhone: "+82 +821091917771", DomainID: 1001, DomainName: "samaneng.com", }, { ID: "works-user-2", Email: "two@example.com", CellPhone: "+821012345678", DomainID: 1001, }, }, } output := &strings.Builder{} count, err := auditWorksmobileDuplicatePhoneCountryCodes(context.Background(), output, true, client) if err != nil { t.Fatalf("auditWorksmobileDuplicatePhoneCountryCodes returned error: %v", err) } if count != 1 { t.Fatalf("count=%d, want 1", count) } if !strings.Contains(output.String(), "one@example.com") || !strings.Contains(output.String(), "+821091917771") { t.Fatalf("audit output did not include normalized duplicate phone row: %s", output.String()) } if len(client.patches) != 1 { t.Fatalf("patch count=%d, want 1", len(client.patches)) } if client.patches[0].identifier != "works-user-1" { t.Fatalf("patch identifier=%q, want works-user-1", client.patches[0].identifier) } if client.patches[0].payload.CellPhone != "+821091917771" { t.Fatalf("patch cellPhone=%q, want +821091917771", client.patches[0].payload.CellPhone) } } func TestRecreatePendingWorksmobileUsersFromSnapshotCreatesOnlyMatchedUsers(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") rootID := "11111111-1111-1111-1111-111111111111" tenantID := "22222222-2222-2222-2222-222222222222" userID := "33333333-3333-3333-3333-333333333333" client := &fakeWorksmobilePendingRecreateClient{} output := &strings.Builder{} writer := csv.NewWriter(output) counts, err := recreatePendingWorksmobileUsersFromSnapshot( context.Background(), []service.WorksmobileRemoteUser{ {Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID, DisplayName: "Matched"}, {Email: "missing@samaneng.com", ID: "works-2", ExternalID: "44444444-4444-4444-4444-444444444444", DisplayName: "Missing"}, }, func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) { if remote.ExternalID != userID { return domain.User{}, false } return domain.User{ ID: userID, Email: "matched@samaneng.com", Name: "Matched User", Status: domain.UserStatusActive, TenantID: &tenantID, }, true }, map[string]domain.Tenant{ rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany}, tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID}, }, nil, "hanmac-family2026", 0, writer, client, ) if err != nil { t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err) } if counts.OK != 1 || counts.Skipped != 1 || counts.Errors != 0 { t.Fatalf("counts=%+v, want ok=1 skipped=1 errors=0", counts) } if len(client.patchedUsers) != 1 || client.patchedUsers[0].identifier != "matched@samaneng.com" { t.Fatalf("patched users=%v", client.patchedUsers) } if !strings.Contains(client.patchedUsers[0].payload.Email, ".old") { t.Fatalf("tombstone email=%q", client.patchedUsers[0].payload.Email) } if len(client.patchedUsers[0].payload.AliasEmails) != 0 { t.Fatalf("tombstone alias emails were not cleared: %v", client.patchedUsers[0].payload.AliasEmails) } if len(client.patchedUsers[0].payload.Organizations) == 0 || client.patchedUsers[0].payload.Organizations[0].Email != client.patchedUsers[0].payload.Email { t.Fatalf("tombstone organization email was not updated: %+v", client.patchedUsers[0].payload.Organizations) } if len(client.deletedUsers) != 1 || client.deletedUsers[0] != client.patchedUsers[0].payload.Email { t.Fatalf("deleted users=%v", client.deletedUsers) } if len(client.createdUsers) != 1 { t.Fatalf("created users=%d, want 1", len(client.createdUsers)) } if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" { t.Fatal("initial password was not applied to recreated user") } if strings.Contains(output.String(), "missing@samaneng.com") && !strings.Contains(output.String(), "baron user not found") { t.Fatalf("missing user skip reason was not written: %s", output.String()) } } func TestRecreatePendingWorksmobileUsersFromSnapshotRollsBackWhenCreateFails(t *testing.T) { t.Setenv("SAMAN_DOMAIN_ID", "1001") rootID := "11111111-1111-1111-1111-111111111111" tenantID := "22222222-2222-2222-2222-222222222222" userID := "33333333-3333-3333-3333-333333333333" client := &fakeWorksmobilePendingRecreateClient{createErr: errors.New("create failed")} output := &strings.Builder{} writer := csv.NewWriter(output) counts, err := recreatePendingWorksmobileUsersFromSnapshot( context.Background(), []service.WorksmobileRemoteUser{{Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID}}, func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) { return domain.User{ ID: userID, Email: "matched@samaneng.com", Name: "Matched User", Status: domain.UserStatusActive, TenantID: &tenantID, }, true }, map[string]domain.Tenant{ rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany}, tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID}, }, nil, "hanmac-family2026", 0, writer, client, ) if err != nil { t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err) } if counts.OK != 0 || counts.Errors != 1 { t.Fatalf("counts=%+v, want ok=0 errors=1", counts) } if len(client.patchedUsers) != 2 { t.Fatalf("patched users=%v", client.patchedUsers) } if client.patchedUsers[1].payload.Email != "matched@samaneng.com" { t.Fatalf("rollback email=%q, want matched@samaneng.com", client.patchedUsers[1].payload.Email) } if !strings.Contains(output.String(), "create failed") || !strings.Contains(output.String(), "ok") { t.Fatalf("rollback result was not written: %s", output.String()) } } func TestImportHanmacWorksmobileUsersFromRowsSkipsExistingRemoteLocalPart(t *testing.T) { t.Setenv("HANMAC_DOMAIN_ID", "300286336") rootID := "11111111-1111-1111-1111-111111111111" companyID := "22222222-2222-2222-2222-222222222222" tenantID := "33333333-3333-3333-3333-333333333333" client := &fakeWorksmobilePendingRecreateClient{} store := &fakeHanmacWorksmobileUserStore{} output := &strings.Builder{} writer := csv.NewWriter(output) counts, err := importHanmacWorksmobileUsersFromRows( context.Background(), []hanmacWorksmobileImportRow{{ Email: "new@hanmaceng.co.kr", Name: "New User", Role: "user", TenantSlug: "infra-structures", EmployeeID: "M25001", SubEmail: "legacy@hanmaceng.co.kr", }}, domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, map[string]domain.Tenant{ "infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, map[string]domain.Tenant{ rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID}, tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, []service.WorksmobileRemoteUser{{ Email: "owner@hanmaceng.co.kr", AliasEmails: []string{"legacy@hanmaceng.co.kr"}, }}, nil, store, "hanmac-family2026", 0, true, writer, client, ) if err != nil { t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err) } if counts.OK != 0 || counts.Skipped != 1 || counts.Errors != 0 { t.Fatalf("counts=%+v, want ok=0 skipped=1 errors=0", counts) } if len(store.saved) != 0 { t.Fatalf("saved users=%d, want 0", len(store.saved)) } if len(client.createdUsers) != 0 { t.Fatalf("created Worksmobile users=%d, want 0", len(client.createdUsers)) } if !strings.Contains(output.String(), "legacy") || !strings.Contains(output.String(), "local-part already exists") { t.Fatalf("result did not include conflict reason: %s", output.String()) } } func TestImportHanmacWorksmobileUsersFromRowsSavesBaronUserAndCreatesWorksmobileUser(t *testing.T) { t.Setenv("HANMAC_DOMAIN_ID", "300286336") rootID := "11111111-1111-1111-1111-111111111111" companyID := "22222222-2222-2222-2222-222222222222" tenantID := "33333333-3333-3333-3333-333333333333" client := &fakeWorksmobilePendingRecreateClient{} store := &fakeHanmacWorksmobileUserStore{} output := &strings.Builder{} writer := csv.NewWriter(output) counts, err := importHanmacWorksmobileUsersFromRows( context.Background(), []hanmacWorksmobileImportRow{{ Email: "new@hanmaceng.co.kr", Name: "New User", Phone: "010-1234-5678", Role: "user", TenantSlug: "infra-structures", Grade: "과장", EmployeeID: "M25001", SubEmail: "new.alias@hanmaceng.co.kr", }}, domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, map[string]domain.Tenant{ "infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, map[string]domain.Tenant{ rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID}, tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, nil, nil, store, "hanmac-family2026", 0, true, writer, client, ) if err != nil { t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err) } if counts.OK != 1 || counts.Skipped != 0 || counts.Errors != 0 || counts.BaronCreated != 1 { t.Fatalf("counts=%+v, want ok=1 baronCreated=1", counts) } if len(store.saved) != 1 { t.Fatalf("saved users=%d, want 1", len(store.saved)) } if store.saved[0].TenantID == nil || *store.saved[0].TenantID != tenantID { t.Fatalf("saved tenant=%v, want %s", store.saved[0].TenantID, tenantID) } if store.saved[0].Metadata["employee_id"] != "M25001" || store.saved[0].Metadata["sub_email"] != "new.alias@hanmaceng.co.kr" { t.Fatalf("metadata=%v", store.saved[0].Metadata) } if len(client.createdUsers) != 1 { t.Fatalf("created Worksmobile users=%d, want 1", len(client.createdUsers)) } if client.createdUsers[0].Email != "new@hanmaceng.co.kr" { t.Fatalf("created email=%q", client.createdUsers[0].Email) } if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" { t.Fatal("initial password was not applied") } if !strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "new.alias@hanmaceng.co.kr") { t.Fatalf("alias emails=%v", client.createdUsers[0].AliasEmails) } } func TestImportHanmacWorksmobileUsersFromRowsKeepsExternalSubEmailOutOfWorksmobileAliases(t *testing.T) { t.Setenv("HANMAC_DOMAIN_ID", "300286336") rootID := "11111111-1111-1111-1111-111111111111" companyID := "22222222-2222-2222-2222-222222222222" tenantID := "33333333-3333-3333-3333-333333333333" client := &fakeWorksmobilePendingRecreateClient{} store := &fakeHanmacWorksmobileUserStore{} output := &strings.Builder{} writer := csv.NewWriter(output) counts, err := importHanmacWorksmobileUsersFromRows( context.Background(), []hanmacWorksmobileImportRow{{ Email: "external-alias@hanmaceng.co.kr", Name: "External Alias", Role: "user", TenantSlug: "infra-structures", EmployeeID: "M25002", SubEmail: "external@gmail.com", }}, domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, map[string]domain.Tenant{ "infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, map[string]domain.Tenant{ rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}, companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID}, tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID}, }, nil, nil, store, "hanmac-family2026", 0, true, writer, client, ) if err != nil { t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err) } if counts.OK != 1 || counts.Errors != 0 || counts.Skipped != 0 { t.Fatalf("counts=%+v, want ok=1", counts) } if store.saved[0].Metadata["sub_email"] != nil { t.Fatalf("external sub_email should not be stored as Worksmobile alias metadata: %v", store.saved[0].Metadata) } if store.saved[0].Metadata["external_sub_email"] != "external@gmail.com" { t.Fatalf("external_sub_email=%v", store.saved[0].Metadata["external_sub_email"]) } if strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "external@gmail.com") { t.Fatalf("external sub email was sent as alias: %v", client.createdUsers[0].AliasEmails) } } func TestBuildAdminctlWorksmobileOrgUnitPayloadClearsDomainRootParent(t *testing.T) { t.Setenv("HANMAC_DOMAIN_ID", "300286336") rootID := "11111111-1111-1111-1111-111111111111" companyID := "22222222-2222-2222-2222-222222222222" orgID := "33333333-3333-3333-3333-333333333333" root := domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"} company := domain.Tenant{ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID} org := domain.Tenant{ID: orgID, Slug: "management-support", Name: "경영지원부", Type: domain.TenantTypeOrganization, ParentID: &companyID} payload, err := buildAdminctlWorksmobileOrgUnitPayload(org, root, map[string]domain.Tenant{ rootID: root, companyID: company, orgID: org, }) if err != nil { t.Fatalf("buildAdminctlWorksmobileOrgUnitPayload returned error: %v", err) } if payload.DomainID != 300286336 { t.Fatalf("domainID=%d, want 300286336", payload.DomainID) } if payload.Email != "management-support@hanmaceng.co.kr" { t.Fatalf("email=%q, want management-support@hanmaceng.co.kr", payload.Email) } if payload.ParentOrgUnitID != "" { t.Fatalf("parentOrgUnitID=%q, want empty for domain-root child", payload.ParentOrgUnitID) } } type fakeWorksmobilePhoneAuditClient struct { users []service.WorksmobileRemoteUser patches []fakeWorksmobilePhonePatch } type fakeWorksmobilePhonePatch struct { identifier string payload service.WorksmobileUserPatchPayload } func (f *fakeWorksmobilePhoneAuditClient) ListUsers(ctx context.Context) ([]service.WorksmobileRemoteUser, error) { return f.users, nil } func (f *fakeWorksmobilePhoneAuditClient) PatchUser(ctx context.Context, identifier string, payload service.WorksmobileUserPatchPayload) error { f.patches = append(f.patches, fakeWorksmobilePhonePatch{identifier: identifier, payload: payload}) return nil } type fakeWorksmobilePendingRecreateClient struct { createdUsers []service.WorksmobileUserPayload deletedUsers []string undeletedUsers []string patchedUsers []fakeWorksmobilePendingRecreatePatch createErr error } type fakeWorksmobilePendingRecreatePatch struct { identifier string payload service.WorksmobileUserPatchPayload } func (f *fakeWorksmobilePendingRecreateClient) CreateOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) UpsertOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload, matchLocalPart string) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) CreateUser(ctx context.Context, payload service.WorksmobileUserPayload) error { if f.createErr != nil { return f.createErr } f.createdUsers = append(f.createdUsers, payload) return nil } func (f *fakeWorksmobilePendingRecreateClient) UpsertUser(ctx context.Context, payload service.WorksmobileUserPayload) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) AddUserAliasEmail(ctx context.Context, userID string, email string) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) ResetUserPassword(ctx context.Context, userID string, password string) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) DeleteUser(ctx context.Context, userID string) error { f.deletedUsers = append(f.deletedUsers, userID) return nil } func (f *fakeWorksmobilePendingRecreateClient) PatchUser(ctx context.Context, identifier string, payload service.WorksmobileUserPatchPayload) error { f.patchedUsers = append(f.patchedUsers, fakeWorksmobilePendingRecreatePatch{identifier: identifier, payload: payload}) return nil } func (f *fakeWorksmobilePendingRecreateClient) SetUserActive(ctx context.Context, userID string, active bool) error { return nil } func (f *fakeWorksmobilePendingRecreateClient) ListUsers(ctx context.Context) ([]service.WorksmobileRemoteUser, error) { return nil, nil } func (f *fakeWorksmobilePendingRecreateClient) ListGroups(ctx context.Context) ([]service.WorksmobileRemoteGroup, error) { return nil, nil } func (f *fakeWorksmobilePendingRecreateClient) UndeleteUser(ctx context.Context, userID string) error { f.undeletedUsers = append(f.undeletedUsers, userID) return nil } type fakeHanmacWorksmobileUserStore struct { users map[string]domain.User saved []domain.User } func (f *fakeHanmacWorksmobileUserStore) FindByEmail(ctx context.Context, email string) (domain.User, bool, error) { if f.users == nil { return domain.User{}, false, nil } user, ok := f.users[strings.ToLower(strings.TrimSpace(email))] return user, ok, nil } func (f *fakeHanmacWorksmobileUserStore) Save(ctx context.Context, user *domain.User) (bool, error) { created := true if f.users == nil { f.users = map[string]domain.User{} } else if _, ok := f.users[strings.ToLower(strings.TrimSpace(user.Email))]; ok { created = false } f.users[strings.ToLower(strings.TrimSpace(user.Email))] = *user f.saved = append(f.saved, *user) return created, nil }