1
0
forked from baron/baron-sso
Files
baron-sso/backend/cmd/adminctl/main_test.go
2026-06-12 18:36:18 +09:00

574 lines
20 KiB
Go

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
}