forked from baron/baron-sso
feat: update worksmobile sync and restore planning
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user