1
0
forked from baron/baron-sso

feat: update worksmobile sync and restore planning

This commit is contained in:
2026-06-01 17:01:53 +09:00
parent 6574fb54b9
commit 5c8a338085
36 changed files with 3922 additions and 243 deletions

View File

@@ -600,6 +600,171 @@ func TestUserHandler_BulkCreateUsers(t *testing.T) {
})
}
func TestUserHandler_BulkCreateUsersPreservesRequestedUserID(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockOry := new(MockOryProvider)
mockTenant := new(MockTenantServiceForUser)
const requestedUserID = "9f8cc1b1-af8d-45d4-946c-924a529c2556"
h := &UserHandler{
KratosAdmin: mockKratos,
OryProvider: mockOry,
TenantService: mockTenant,
}
app.Post("/users/bulk", h.BulkCreateUsers)
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
mockTenant.On("GetTenant", mock.Anything, "tenant-123").Return(&domain.Tenant{
ID: "tenant-123",
Slug: "restore-tenant",
Config: domain.JSONMap{},
}, nil).Once()
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
return user != nil && user.ID == requestedUserID && user.Email == "restore@test.com"
}), mock.Anything).Return(requestedUserID, nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"userId": requestedUserID,
"email": "restore@test.com",
"name": "Restore User",
"tenantId": "tenant-123",
"tenantSlug": "restore-tenant",
"metadata": map[string]any{},
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
results := result["results"].([]any)
require.Len(t, results, 1)
row := results[0].(map[string]any)
assert.True(t, row["success"].(bool))
assert.Equal(t, requestedUserID, row["userId"])
mockOry.AssertExpectations(t)
}
func TestUserHandler_BulkCreateUsersRejectsDuplicateAliasEmailsInBatch(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)
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil).Once()
payload := map[string]interface{}{
"users": []map[string]interface{}{
{
"email": "user1@samaneng.com",
"name": "User One",
"tenantSlug": "rnd-saman",
"metadata": map[string]interface{}{
"sub_email": []interface{}{"shared@hanmaceng.co.kr"},
},
},
{
"email": "user2@samaneng.com",
"name": "User Two",
"tenantSlug": "rnd-saman",
"metadata": map[string]interface{}{
"worksmobileAliasEmails": []interface{}{"shared@hanmaceng.co.kr"},
},
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
results := result["results"].([]interface{})
require.Len(t, results, 2)
for _, item := range results {
row := item.(map[string]interface{})
require.False(t, row["success"].(bool))
require.Equal(t, "blockingError", row["status"])
require.Contains(t, row["message"].(string), "duplicate email")
}
mockOry.AssertExpectations(t)
mockOry.AssertNotCalled(t, "CreateUser", mock.Anything, mock.Anything)
mockTenant.AssertNotCalled(t, "GetTenantBySlug", mock.Anything, mock.Anything)
}
func TestUserHandler_BulkCreateUsersRejectsPrimaryEmailUsedAsSubEmail(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)
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil).Once()
payload := map[string]interface{}{
"users": []map[string]interface{}{
{
"email": "user1@samaneng.com",
"name": "User One",
"tenantSlug": "rnd-saman",
"metadata": map[string]interface{}{
"sub_email": []interface{}{"user2@samaneng.com"},
},
},
{
"email": "user2@samaneng.com",
"name": "User Two",
"tenantSlug": "rnd-saman",
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
results := result["results"].([]interface{})
require.Len(t, results, 2)
for _, item := range results {
row := item.(map[string]interface{})
require.False(t, row["success"].(bool))
require.Equal(t, "blockingError", row["status"])
require.Contains(t, row["message"].(string), "duplicate email")
}
mockOry.AssertExpectations(t)
mockOry.AssertNotCalled(t, "CreateUser", mock.Anything, mock.Anything)
mockTenant.AssertNotCalled(t, "GetTenantBySlug", mock.Anything, mock.Anything)
}
func TestUserHandler_BulkCreateUsers_ResolvesAdditionalAppointment(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
@@ -1429,6 +1594,138 @@ func TestUserHandler_UpdateUser_RejectsDeprecatedAdminRoles(t *testing.T) {
mockKratos.AssertExpectations(t)
}
func TestUserHandler_UpdateUser_AllowsSuperAdminEmailChange(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
h := &UserHandler{KratosAdmin: mockKratos}
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin-1", Role: domain.RoleSuperAdmin})
return h.UpdateUser(c)
})
userID := "u-1"
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "old@example.com",
"name": "사용자",
"role": domain.RoleUser,
},
State: "active",
}, nil).Once()
mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool {
return traits["email"] == "new@example.com"
}), "").Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "new@example.com",
"name": "사용자",
"role": domain.RoleUser,
},
State: "active",
}, nil).Once()
body, _ := json.Marshal(map[string]interface{}{"email": "new@example.com"})
req := httptest.NewRequest(http.MethodPut, "/users/"+userID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
mockKratos.AssertExpectations(t)
}
func TestUserHandler_UpdateUserClearsWorksmobileAliasMetadataWhenSubEmailIsCleared(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
h := &UserHandler{KratosAdmin: mockKratos}
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "admin-1", Role: domain.RoleSuperAdmin})
return h.UpdateUser(c)
})
userID := "u-1"
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "user@example.com",
"name": "사용자",
"role": domain.RoleUser,
"sub_email": []interface{}{"alias@hanmaceng.co.kr"},
"aliasEmails": []interface{}{"alias@hanmaceng.co.kr"},
"secondary_emails": []interface{}{"alias@hanmaceng.co.kr"},
"worksmobileAliasEmails": []interface{}{"alias@hanmaceng.co.kr"},
},
State: "active",
}, nil).Once()
mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool {
for _, key := range []string{"sub_email", "aliasEmails", "secondary_emails", "worksmobileAliasEmails"} {
values, ok := traits[key].([]interface{})
if !ok || len(values) != 0 {
return false
}
}
return true
}), "").Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "user@example.com",
"name": "사용자",
"role": domain.RoleUser,
"sub_email": []interface{}{},
"aliasEmails": []interface{}{},
"secondary_emails": []interface{}{},
"worksmobileAliasEmails": []interface{}{},
},
State: "active",
}, nil).Once()
body, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"sub_email": []interface{}{},
},
})
req := httptest.NewRequest(http.MethodPut, "/users/"+userID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
mockKratos.AssertExpectations(t)
}
func TestUserHandler_UpdateUser_RejectsNonSuperAdminEmailChange(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
h := &UserHandler{KratosAdmin: mockKratos}
app.Put("/users/:id", func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-2", Role: domain.RoleUser})
return h.UpdateUser(c)
})
userID := "u-1"
mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{
ID: userID,
Traits: map[string]interface{}{
"email": "old@example.com",
"name": "사용자",
"role": domain.RoleUser,
},
State: "active",
}, nil).Once()
body, _ := json.Marshal(map[string]interface{}{"email": "new@example.com"})
req := httptest.NewRequest(http.MethodPut, "/users/"+userID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
mockKratos.AssertExpectations(t)
mockKratos.AssertNotCalled(t, "UpdateIdentity", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
}
func TestSyncCustomLoginIDs_IgnoresFlatMetadataMaps(t *testing.T) {
mockTenant := new(MockTenantServiceForUser)
tenantID := "tenant-uuid"