forked from baron/baron-sso
동기화 기초구조 마련
This commit is contained in:
@@ -628,6 +628,7 @@ func normalizeTenantDomainInputs(values []string) []string {
|
||||
|
||||
func normalizeTenantConfig(config map[string]any) (domain.JSONMap, error) {
|
||||
normalized := make(domain.JSONMap, len(config))
|
||||
orgUnitTypeError := "orgUnitType must be one of 실, 팀, TF, TF팀, 센터, 디비전, 셀, 본부, 지역본부, 부, 임원직속"
|
||||
for key, value := range config {
|
||||
if key == "userSchema" {
|
||||
fields, err := normalizeTenantUserSchema(value)
|
||||
@@ -656,14 +657,14 @@ func normalizeTenantConfig(config map[string]any) (domain.JSONMap, error) {
|
||||
if key == "orgUnitType" {
|
||||
orgUnitType, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("orgUnitType must be one of 실, 팀, 디비전, 셀, 본부, 지역본부, 부")
|
||||
return nil, errors.New(orgUnitTypeError)
|
||||
}
|
||||
orgUnitType = strings.TrimSpace(orgUnitType)
|
||||
if orgUnitType == "" {
|
||||
continue
|
||||
}
|
||||
if !isAllowedOrgUnitType(orgUnitType) {
|
||||
return nil, fmt.Errorf("orgUnitType must be one of 실, 팀, 디비전, 셀, 본부, 지역본부, 부")
|
||||
return nil, errors.New(orgUnitTypeError)
|
||||
}
|
||||
normalized[key] = orgUnitType
|
||||
continue
|
||||
@@ -675,7 +676,7 @@ func normalizeTenantConfig(config map[string]any) (domain.JSONMap, error) {
|
||||
|
||||
func isAllowedOrgUnitType(value string) bool {
|
||||
switch value {
|
||||
case "실", "팀", "디비전", "셀", "본부", "지역본부", "부":
|
||||
case "실", "팀", "TF", "TF팀", "센터", "디비전", "셀", "본부", "지역본부", "부", "임원직속":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -730,12 +730,25 @@ func TestNormalizeTenantConfigRejectsNonTextLoginIDFields(t *testing.T) {
|
||||
func TestNormalizeTenantConfigAcceptsTenantVisibilityAndOrgUnitType(t *testing.T) {
|
||||
config, err := normalizeTenantConfig(map[string]any{
|
||||
"visibility": "internal",
|
||||
"orgUnitType": "팀",
|
||||
"orgUnitType": "센터",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "internal", config["visibility"])
|
||||
assert.Equal(t, "팀", config["orgUnitType"])
|
||||
assert.Equal(t, "센터", config["orgUnitType"])
|
||||
}
|
||||
|
||||
func TestNormalizeTenantConfigAcceptsTaskForceAndExecutiveOrgUnitTypes(t *testing.T) {
|
||||
for _, orgUnitType := range []string{"TF", "TF팀", "임원직속"} {
|
||||
t.Run(orgUnitType, func(t *testing.T) {
|
||||
config, err := normalizeTenantConfig(map[string]any{
|
||||
"orgUnitType": orgUnitType,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, orgUnitType, config["orgUnitType"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeTenantConfigRejectsInvalidTenantVisibility(t *testing.T) {
|
||||
|
||||
@@ -118,6 +118,47 @@ func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any,
|
||||
return ""
|
||||
}
|
||||
|
||||
func bulkUserEmailDomainCandidates(emailDomain string, email string) []string {
|
||||
values := make([]string, 0, 2)
|
||||
seen := map[string]bool{}
|
||||
add := func(value string) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized == "" || seen[normalized] {
|
||||
return
|
||||
}
|
||||
seen[normalized] = true
|
||||
values = append(values, normalized)
|
||||
}
|
||||
for _, value := range strings.FieldsFunc(emailDomain, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '\n' || r == '\r'
|
||||
}) {
|
||||
add(value)
|
||||
}
|
||||
if _, domainPart, err := domain.SplitEmailDomain(email); err == nil {
|
||||
add(domainPart)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func bulkUserAssignmentContainsTenant(appointments []any, primaryTenantID string, tenantID string) bool {
|
||||
if strings.TrimSpace(tenantID) == "" {
|
||||
return true
|
||||
}
|
||||
if primaryTenantID != "" && primaryTenantID == tenantID {
|
||||
return true
|
||||
}
|
||||
for _, item := range appointments {
|
||||
appointment, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if normalizeMetadataString(appointment["tenantId"]) == tenantID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func metadataBoolFromMap(metadata map[string]any, keys ...string) (bool, bool) {
|
||||
for _, key := range keys {
|
||||
value, ok := metadata[key]
|
||||
@@ -664,17 +705,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type bulkUserItem struct {
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
Department string `json:"department"`
|
||||
Grade string `json:"grade"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
TenantID string `json:"tenantId"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
Department string `json:"department"`
|
||||
Grade string `json:"grade"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
AdditionalAppointments []map[string]any `json:"additionalAppointments"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type bulkUserResult struct {
|
||||
@@ -720,15 +764,103 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
// Pre-fetch tenant data to avoid redundant DB calls
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Slug string
|
||||
Name string
|
||||
Schema []interface{}
|
||||
Groups []domain.UserGroup
|
||||
LoginIDField string
|
||||
}
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
tenantCacheByID := make(map[string]tenantCacheItem)
|
||||
tenantCacheByDomain := make(map[string]tenantCacheItem)
|
||||
|
||||
buildTenantCacheItem := func(tenant *domain.Tenant) tenantCacheItem {
|
||||
tItem := tenantCacheItem{
|
||||
ID: tenant.ID,
|
||||
Slug: tenant.Slug,
|
||||
Name: tenant.Name,
|
||||
}
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
}
|
||||
if lf, ok := tenant.Config["loginIdField"].(string); ok {
|
||||
tItem.LoginIDField = lf
|
||||
}
|
||||
if h.UserGroupRepo != nil {
|
||||
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
|
||||
tItem.Groups = groups
|
||||
}
|
||||
}
|
||||
return tItem
|
||||
}
|
||||
|
||||
cacheTenantItem := func(tItem tenantCacheItem) tenantCacheItem {
|
||||
if tItem.Slug != "" {
|
||||
tenantCache[strings.ToLower(strings.TrimSpace(tItem.Slug))] = tItem
|
||||
}
|
||||
if tItem.ID != "" {
|
||||
tenantCacheByID[tItem.ID] = tItem
|
||||
}
|
||||
return tItem
|
||||
}
|
||||
|
||||
resolveTenantBySlug := func(slug string) (tenantCacheItem, error) {
|
||||
normalizedSlug := strings.ToLower(strings.TrimSpace(slug))
|
||||
if normalizedSlug == "" {
|
||||
return tenantCacheItem{}, errors.New("tenantSlug is required")
|
||||
}
|
||||
if tItem, exists := tenantCache[normalizedSlug]; exists {
|
||||
return tItem, nil
|
||||
}
|
||||
if h.TenantService == nil {
|
||||
return tenantCacheItem{}, errors.New("tenant service unavailable")
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), normalizedSlug)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, errors.New("invalid tenantSlug: tenant not found")
|
||||
}
|
||||
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
|
||||
}
|
||||
|
||||
resolveTenantByID := func(tenantID string) (tenantCacheItem, error) {
|
||||
normalizedID := strings.TrimSpace(tenantID)
|
||||
if normalizedID == "" {
|
||||
return tenantCacheItem{}, errors.New("tenantId is required")
|
||||
}
|
||||
if tItem, exists := tenantCacheByID[normalizedID]; exists {
|
||||
return tItem, nil
|
||||
}
|
||||
if h.TenantService == nil {
|
||||
return tenantCacheItem{}, errors.New("tenant service unavailable")
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenant(c.Context(), normalizedID)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, errors.New("invalid tenantId: tenant not found")
|
||||
}
|
||||
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
|
||||
}
|
||||
|
||||
resolveTenantByDomain := func(domainName string) (tenantCacheItem, bool) {
|
||||
normalizedDomain := strings.ToLower(strings.TrimSpace(domainName))
|
||||
if normalizedDomain == "" || h.TenantService == nil {
|
||||
return tenantCacheItem{}, false
|
||||
}
|
||||
if tItem, exists := tenantCacheByDomain[normalizedDomain]; exists {
|
||||
return tItem, true
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), normalizedDomain)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, false
|
||||
}
|
||||
tItem := cacheTenantItem(buildTenantCacheItem(tenant))
|
||||
tenantCacheByDomain[normalizedDomain] = tItem
|
||||
return tItem, true
|
||||
}
|
||||
|
||||
for _, item := range req.Users {
|
||||
email := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
tenantID := strings.TrimSpace(item.TenantID)
|
||||
tenantSlug := strings.TrimSpace(item.TenantSlug)
|
||||
dept := strings.TrimSpace(item.Department)
|
||||
|
||||
@@ -737,9 +869,38 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if tenantSlug == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantSlug is required"})
|
||||
continue
|
||||
var tItem tenantCacheItem
|
||||
var err error
|
||||
if tenantID != "" {
|
||||
tItem, err = resolveTenantByID(tenantID)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
if tenantSlug != "" && !strings.EqualFold(tenantSlug, tItem.Slug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantId and tenantSlug do not match"})
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
} else if tenantSlug != "" {
|
||||
tItem, err = resolveTenantBySlug(tenantSlug)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
} else {
|
||||
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, email) {
|
||||
if domainTenant, ok := resolveTenantByDomain(domainName); ok {
|
||||
tItem = domainTenant
|
||||
tenantSlug = domainTenant.Slug
|
||||
break
|
||||
}
|
||||
}
|
||||
if tenantSlug == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant assignment is required"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
@@ -750,33 +911,47 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Tenant Existence and Resolve ID (with Cache)
|
||||
var tItem tenantCacheItem
|
||||
var exists bool
|
||||
if tItem, exists = tenantCache[tenantSlug]; !exists {
|
||||
if h.TenantService != nil {
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
|
||||
if err != nil || tenant == nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "invalid tenantSlug: tenant not found"})
|
||||
resolvedAppointments := make([]any, 0, len(item.AdditionalAppointments)+2)
|
||||
if len(item.AdditionalAppointments) > 0 {
|
||||
appointmentFailed := false
|
||||
for _, rawAppointment := range item.AdditionalAppointments {
|
||||
appointmentTenantSlug := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantSlug"]))
|
||||
if appointmentTenantSlug == "" {
|
||||
continue
|
||||
}
|
||||
tItem.ID = tenant.ID
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin && appointmentTenantSlug != requester.CompanyCode {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
if lf, ok := tenant.Config["loginIdField"].(string); ok {
|
||||
tItem.LoginIDField = lf
|
||||
}
|
||||
// [Fix] Cache user groups for this tenant to match department
|
||||
if h.UserGroupRepo != nil {
|
||||
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
|
||||
tItem.Groups = groups
|
||||
|
||||
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
|
||||
if !exists {
|
||||
appointmentTenant, err = resolveTenantBySlug(appointmentTenantSlug)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: strings.Replace(err.Error(), "tenantSlug", "additional tenantSlug", 1)})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tenantCache[tenantSlug] = tItem
|
||||
} else {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant service unavailable"})
|
||||
appointment := make(map[string]any, len(rawAppointment)+3)
|
||||
for key, value := range rawAppointment {
|
||||
if key == "tenantSlug" || key == "tenantId" || key == "tenantName" {
|
||||
continue
|
||||
}
|
||||
appointment[key] = value
|
||||
}
|
||||
appointment["tenantId"] = appointmentTenant.ID
|
||||
appointment["tenantSlug"] = appointmentTenant.Slug
|
||||
if name := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantName"])); name != "" {
|
||||
appointment["tenantName"] = name
|
||||
} else {
|
||||
appointment["tenantName"] = appointmentTenant.Name
|
||||
}
|
||||
resolvedAppointments = append(resolvedAppointments, appointment)
|
||||
}
|
||||
if appointmentFailed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -836,6 +1011,26 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, userEmail) {
|
||||
domainTenant, ok := resolveTenantByDomain(domainName)
|
||||
if !ok || bulkUserAssignmentContainsTenant(resolvedAppointments, tItem.ID, domainTenant.ID) {
|
||||
continue
|
||||
}
|
||||
resolvedAppointments = append(resolvedAppointments, map[string]any{
|
||||
"tenantId": domainTenant.ID,
|
||||
"tenantSlug": domainTenant.Slug,
|
||||
"tenantName": domainTenant.Name,
|
||||
"assignmentSource": "email_domain",
|
||||
"sourceDomain": strings.ToLower(strings.TrimSpace(domainName)),
|
||||
})
|
||||
}
|
||||
if len(resolvedAppointments) > 0 {
|
||||
if item.Metadata == nil {
|
||||
item.Metadata = map[string]any{}
|
||||
}
|
||||
item.Metadata["additionalAppointments"] = resolvedAppointments
|
||||
}
|
||||
|
||||
password, _ := utils.GeneratePasswordWithPolicy(policy)
|
||||
role := item.Role
|
||||
if role == "" {
|
||||
|
||||
@@ -139,6 +139,19 @@ func (m *MockTenantServiceForUser) GetTenant(ctx context.Context, id string) (*d
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForUser) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
|
||||
for _, call := range m.ExpectedCalls {
|
||||
if call.Method == "GetTenantByDomain" {
|
||||
args := m.Called(ctx, emailDomain)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForUser) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) == nil {
|
||||
@@ -432,6 +445,217 @@ func TestUserHandler_BulkCreateUsers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkCreateUsers_ResolvesAdditionalAppointment(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockOry := new(MockOryProvider)
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
OryProvider: mockOry,
|
||||
TenantService: mockTenant,
|
||||
}
|
||||
app.Post("/users/bulk", h.BulkCreateUsers)
|
||||
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
|
||||
ID: "t-primary",
|
||||
Slug: "test-tenant",
|
||||
Name: "Primary Tenant",
|
||||
Config: domain.JSONMap{},
|
||||
}, nil).Once()
|
||||
mockTenant.On("GetTenant", mock.Anything, "t-primary").Return(&domain.Tenant{
|
||||
ID: "t-primary",
|
||||
Slug: "test-tenant",
|
||||
Name: "Primary Tenant",
|
||||
Config: domain.JSONMap{},
|
||||
}, nil)
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "second-tenant").Return(&domain.Tenant{
|
||||
ID: "t-second",
|
||||
Slug: "second-tenant",
|
||||
Name: "Second Tenant",
|
||||
Config: domain.JSONMap{},
|
||||
}, nil).Once()
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
|
||||
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
|
||||
appointments, ok := user.Attributes["additionalAppointments"].([]any)
|
||||
if !ok || len(appointments) != 1 {
|
||||
return false
|
||||
}
|
||||
appointment, ok := appointments[0].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
metadata, _ := appointment["metadata"].(map[string]any)
|
||||
return appointment["tenantId"] == "t-second" &&
|
||||
appointment["tenantSlug"] == "second-tenant" &&
|
||||
appointment["tenantName"] == "Second Tenant" &&
|
||||
appointment["department"] == "센터" &&
|
||||
appointment["grade"] == "수석" &&
|
||||
appointment["jobTitle"] == "Architecture" &&
|
||||
metadata["employee_id"] == "EMP002"
|
||||
}), mock.Anything).Return("u-appointment", nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{
|
||||
"email": "dual@test.com",
|
||||
"name": "Dual User",
|
||||
"tenantSlug": "test-tenant",
|
||||
"metadata": map[string]interface{}{"employee_id": "EMP001"},
|
||||
"additionalAppointments": []map[string]interface{}{
|
||||
{
|
||||
"tenantSlug": "second-tenant",
|
||||
"department": "센터",
|
||||
"grade": "수석",
|
||||
"jobTitle": "Architecture",
|
||||
"metadata": map[string]interface{}{"employee_id": "EMP002"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req)
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
mockTenant.AssertExpectations(t)
|
||||
mockOry.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkCreateUsers_AppendsEmailDomainTenantAtLowestPriority(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockOry := new(MockOryProvider)
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
OryProvider: mockOry,
|
||||
TenantService: mockTenant,
|
||||
}
|
||||
app.Post("/users/bulk", h.BulkCreateUsers)
|
||||
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "gpdtdc").Return(&domain.Tenant{
|
||||
ID: "t-gpdtdc",
|
||||
Slug: "gpdtdc",
|
||||
Name: "GPDTDC",
|
||||
Config: domain.JSONMap{},
|
||||
}, nil).Once()
|
||||
mockTenant.On("GetTenant", mock.Anything, "t-gpdtdc").Return(&domain.Tenant{
|
||||
ID: "t-gpdtdc",
|
||||
Slug: "gpdtdc",
|
||||
Name: "GPDTDC",
|
||||
Config: domain.JSONMap{},
|
||||
}, nil)
|
||||
mockTenant.On("GetTenantByDomain", mock.Anything, "samaneng.com").Return(&domain.Tenant{
|
||||
ID: "t-saman",
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
Status: domain.TenantStatusActive,
|
||||
Config: domain.JSONMap{},
|
||||
}, nil).Once()
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
|
||||
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
|
||||
if user.Attributes["tenant_id"] != "t-gpdtdc" || user.Attributes["companyCode"] != "gpdtdc" {
|
||||
return false
|
||||
}
|
||||
appointments, ok := user.Attributes["additionalAppointments"].([]any)
|
||||
if !ok || len(appointments) != 1 {
|
||||
return false
|
||||
}
|
||||
appointment, ok := appointments[0].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return appointment["tenantId"] == "t-saman" &&
|
||||
appointment["tenantSlug"] == "saman" &&
|
||||
appointment["tenantName"] == "삼안" &&
|
||||
appointment["assignmentSource"] == "email_domain" &&
|
||||
appointment["sourceDomain"] == "samaneng.com"
|
||||
}), mock.Anything).Return("u-domain-assigned", nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{
|
||||
"email": "user@samaneng.com",
|
||||
"name": "Domain User",
|
||||
"tenantSlug": "gpdtdc",
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
results := result["results"].([]interface{})
|
||||
assert.True(t, results[0].(map[string]interface{})["success"].(bool))
|
||||
mockTenant.AssertExpectations(t)
|
||||
mockOry.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkCreateUsers_UsesEmailDomainTenantAsPrimaryWhenExplicitTenantMissing(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockOry := new(MockOryProvider)
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
OryProvider: mockOry,
|
||||
TenantService: mockTenant,
|
||||
}
|
||||
app.Post("/users/bulk", h.BulkCreateUsers)
|
||||
|
||||
mockTenant.On("GetTenantByDomain", mock.Anything, "samaneng.com").Return(&domain.Tenant{
|
||||
ID: "t-saman",
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
Status: domain.TenantStatusActive,
|
||||
Config: domain.JSONMap{},
|
||||
}, nil).Once()
|
||||
mockTenant.On("GetTenant", mock.Anything, "t-saman").Return(&domain.Tenant{
|
||||
ID: "t-saman",
|
||||
Slug: "saman",
|
||||
Name: "삼안",
|
||||
Status: domain.TenantStatusActive,
|
||||
Config: domain.JSONMap{},
|
||||
}, nil)
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
|
||||
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
|
||||
return user.Attributes["tenant_id"] == "t-saman" &&
|
||||
user.Attributes["companyCode"] == "saman" &&
|
||||
user.Attributes["additionalAppointments"] == nil
|
||||
}), mock.Anything).Return("u-domain-primary", nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{
|
||||
"email": "user@samaneng.com",
|
||||
"name": "Domain Primary User",
|
||||
},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
results := result["results"].([]interface{})
|
||||
assert.True(t, results[0].(map[string]interface{})["success"].(bool))
|
||||
mockTenant.AssertExpectations(t)
|
||||
mockOry.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_ListUsersReturnsServiceUnavailableWhenKratosFails(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
|
||||
Reference in New Issue
Block a user