forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
@@ -126,6 +126,14 @@ func (m *MockTenantServiceForUser) ListManageableTenants(ctx context.Context, us
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForUser) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Get(1).(int64), args.Error(2)
|
||||
}
|
||||
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForUser) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, domainName)
|
||||
if args.Get(0) == nil {
|
||||
@@ -167,20 +175,66 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
|
||||
},
|
||||
}, int64(1), nil).Once()
|
||||
|
||||
req := httptest.NewRequest("GET", "/users/export?tenantSlug=test-tenant", nil)
|
||||
req := httptest.NewRequest("GET", "/users/export?tenantSlug=test-tenant&includeIds=true", nil)
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
body := strings.TrimPrefix(string(bodyBytes), "\ufeff")
|
||||
assert.Contains(t, body, "ID,Email,Name,Phone,Status,Tenant,Position,JobTitle,CreatedAt")
|
||||
assert.Contains(t, body, "u-1,user@test.com,Test User,010-1111-2222,active,test-tenant")
|
||||
assert.Contains(t, body, "user_id,Email,Name,Phone,Status,tenant_id,tenant_slug,Position,JobTitle,CreatedAt")
|
||||
assert.Contains(t, body, "u-1,user@test.com,Test User,010-1111-2222,active,,test-tenant")
|
||||
assert.NotContains(t, body, "Role")
|
||||
assert.NotContains(t, body, "Department")
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockRepo := new(MockUserRepoForHandler)
|
||||
h := &UserHandler{UserRepo: mockRepo}
|
||||
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{
|
||||
Role: domain.RoleSuperAdmin,
|
||||
})
|
||||
return c.Next()
|
||||
})
|
||||
app.Get("/users/export", h.ExportUsersCSV)
|
||||
|
||||
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
|
||||
tenantID := "tenant-uuid"
|
||||
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
|
||||
Return([]domain.User{
|
||||
{
|
||||
ID: "user-uuid",
|
||||
Email: "user@test.com",
|
||||
Name: "Test User",
|
||||
Phone: "010-1111-2222",
|
||||
Status: "active",
|
||||
CompanyCode: "test-tenant",
|
||||
TenantID: &tenantID,
|
||||
Position: "책임",
|
||||
JobTitle: "플랫폼 운영",
|
||||
CreatedAt: createdAt,
|
||||
},
|
||||
}, int64(1), nil).Once()
|
||||
|
||||
req := httptest.NewRequest("GET", "/users/export?includeIds=false", nil)
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
body := strings.TrimPrefix(string(bodyBytes), "\ufeff")
|
||||
assert.Contains(t, body, "Email,Name,Phone,Status,tenant_slug,Position,JobTitle,CreatedAt")
|
||||
assert.Contains(t, body, "user@test.com,Test User,010-1111-2222,active,test-tenant")
|
||||
assert.NotContains(t, body, "user-uuid")
|
||||
assert.NotContains(t, body, "tenant-uuid")
|
||||
assert.NotContains(t, body, "ID,")
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkCreateUsers(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
@@ -355,6 +409,170 @@ func TestUserHandler_BulkCreateUsers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockOry := new(MockOryProvider)
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
mockRepo := new(MockUserRepoForHandler)
|
||||
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
OryProvider: mockOry,
|
||||
TenantService: mockTenant,
|
||||
UserRepo: mockRepo,
|
||||
}
|
||||
|
||||
app.Post("/users/bulk", h.BulkCreateUsers)
|
||||
|
||||
rootID := "hanmac-family-id"
|
||||
companyID := "hanmac-id"
|
||||
tenants := []domain.Tenant{
|
||||
{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
{ID: companyID, Slug: "hanmac", Name: "한맥기술", ParentID: &rootID},
|
||||
{ID: "external-id", Slug: "external", Name: "외부사"},
|
||||
}
|
||||
|
||||
t.Run("domain only email receives suggested final email with next suffix", func(t *testing.T) {
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "hanmac").Return(&domain.Tenant{
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Once()
|
||||
mockTenant.On("GetTenant", mock.Anything, companyID).Return(&domain.Tenant{
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID}).Return([]domain.User{
|
||||
{Email: "cyhan@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
{Email: "cyhan1@samaneng.com", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
}, nil).Once()
|
||||
mockRepo.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac"}).Return([]domain.User{}, nil).Once()
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil).Once()
|
||||
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
|
||||
return user.Email == "cyhan2@hanmaceng.co.kr"
|
||||
}), mock.Anything).Return("u-hanmac", nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{
|
||||
"email": "@hanmaceng.co.kr",
|
||||
"name": "한치영",
|
||||
"tenantSlug": "hanmac",
|
||||
},
|
||||
},
|
||||
}
|
||||
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{})
|
||||
row := results[0].(map[string]interface{})
|
||||
assert.True(t, row["success"].(bool))
|
||||
assert.Equal(t, "cyhan2@hanmaceng.co.kr", row["email"])
|
||||
assert.Equal(t, "@hanmaceng.co.kr", row["originalEmail"])
|
||||
assert.Contains(t, row["warnings"].([]interface{}), "suggested")
|
||||
})
|
||||
|
||||
t.Run("full email duplicate local part is blocking error", func(t *testing.T) {
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "hanmac").Return(&domain.Tenant{
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Once()
|
||||
mockTenant.On("GetTenant", mock.Anything, companyID).Return(&domain.Tenant{
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID}).Return([]domain.User{
|
||||
{Email: "han@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
}, nil).Once()
|
||||
mockRepo.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac"}).Return([]domain.User{}, nil).Once()
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{
|
||||
"email": "han@samaneng.com",
|
||||
"name": "한치영",
|
||||
"tenantSlug": "hanmac",
|
||||
},
|
||||
},
|
||||
}
|
||||
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{})
|
||||
row := results[0].(map[string]interface{})
|
||||
assert.False(t, row["success"].(bool))
|
||||
assert.Equal(t, "blockingError", row["status"])
|
||||
assert.Contains(t, row["message"].(string), "한맥가족 내에서 이미 사용 중인 이메일 ID입니다.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandler_CreateUser_HanmacEmailPolicyBlocksDuplicateLocalPart(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
mockOry := new(MockOryProvider)
|
||||
mockTenant := new(MockTenantServiceForUser)
|
||||
mockRepo := new(MockUserRepoForHandler)
|
||||
|
||||
h := &UserHandler{
|
||||
KratosAdmin: mockKratos,
|
||||
OryProvider: mockOry,
|
||||
TenantService: mockTenant,
|
||||
UserRepo: mockRepo,
|
||||
}
|
||||
|
||||
app.Post("/users", h.CreateUser)
|
||||
|
||||
rootID := "hanmac-family-id"
|
||||
companyID := "hanmac-id"
|
||||
tenants := []domain.Tenant{
|
||||
{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
{ID: companyID, Slug: "hanmac", Name: "한맥기술", ParentID: &rootID},
|
||||
}
|
||||
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil).Once()
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "hanmac").Return(&domain.Tenant{
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Once()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID}).Return([]domain.User{
|
||||
{Email: "han@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
}, nil).Once()
|
||||
mockRepo.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac"}).Return([]domain.User{}, nil).Once()
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"email": "han@samaneng.com",
|
||||
"name": "한치영",
|
||||
"companyCode": "hanmac",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req)
|
||||
assert.Equal(t, http.StatusConflict, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
assert.Contains(t, result["error"].(string), "한맥가족 내에서 이미 사용 중인 이메일 ID입니다.")
|
||||
mockOry.AssertNotCalled(t, "CreateUser")
|
||||
}
|
||||
|
||||
func TestUserHandler_BulkUpdateUsers(t *testing.T) {
|
||||
app := fiber.New()
|
||||
mockKratos := new(MockKratosAdmin)
|
||||
|
||||
Reference in New Issue
Block a user