1
0
forked from baron/baron-sso

feat(user): support fixed UUID registration and enhance bulk import results

- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping)
- Implemented idempotency and visibility restoration for soft-deleted users
- Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields
- Added logic to reclaim identifiers (login_id) from colliding records
- Added frontend E2E and backend unit tests for UUID integrity and conflict handling
- Fixed i18n, formatting, and mock tests to satisfy code-check
- Applied 'go fix' for 'omitzero' tags and general Go standards
This commit is contained in:
2026-06-01 15:34:08 +09:00
parent 4a1e89e421
commit 31d107ff2e
85 changed files with 2104 additions and 1149 deletions

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"maps"
"net/http"
"net/http/httptest"
"testing"
@@ -33,7 +34,7 @@ func (r *recordingUpdateMeUserRepo) UpdateUserLoginIDs(ctx context.Context, user
func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
token := "token-abc"
identityID := "user-1"
traits := map[string]interface{}{
traits := map[string]any{
"email": "qa@example.com",
"name": "QA User",
"phone_number": "+821012345678",
@@ -51,8 +52,8 @@ func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
if r.Header.Get("X-Session-Token") != token {
return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil
}
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
return httpJSONAny(r, http.StatusOK, map[string]any{
"identity": map[string]any{
"id": identityID,
"traits": traits,
},
@@ -62,14 +63,12 @@ func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
r.URL.Path == "/admin/identities/"+identityID &&
r.Method == http.MethodPut:
var payload struct {
Traits map[string]interface{} `json:"traits"`
Traits map[string]any `json:"traits"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil
}
for k, v := range payload.Traits {
traits[k] = v
}
maps.Copy(traits, payload.Traits)
return httpResponse(r, http.StatusOK, `{"ok":true}`), nil
}
@@ -93,7 +92,7 @@ func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
getResp1, err := app.Test(getReq1, -1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, getResp1.StatusCode)
var profile1 map[string]interface{}
var profile1 map[string]any
require.NoError(t, json.NewDecoder(getResp1.Body).Decode(&profile1))
require.Equal(t, "Old Dept", profile1["department"])
@@ -121,7 +120,7 @@ func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
getResp2, err := app.Test(getReq2, -1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, getResp2.StatusCode)
var profile2 map[string]interface{}
var profile2 map[string]any
require.NoError(t, json.NewDecoder(getResp2.Body).Decode(&profile2))
require.Equal(t, "New Dept", profile2["department"])
}
@@ -129,7 +128,7 @@ func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
func TestUpdateMe_SyncsLocalReadModelFields(t *testing.T) {
token := "token-sync"
identityID := "user-sync"
traits := map[string]interface{}{
traits := map[string]any{
"email": "sync@example.com",
"name": "Old Name",
"phone_number": "+821012345678",
@@ -148,8 +147,8 @@ func TestUpdateMe_SyncsLocalReadModelFields(t *testing.T) {
if r.Header.Get("X-Session-Token") != token {
return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil
}
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
return httpJSONAny(r, http.StatusOK, map[string]any{
"identity": map[string]any{
"id": identityID,
"traits": traits,
},
@@ -159,14 +158,12 @@ func TestUpdateMe_SyncsLocalReadModelFields(t *testing.T) {
r.URL.Path == "/admin/identities/"+identityID &&
r.Method == http.MethodPut:
var payload struct {
Traits map[string]interface{} `json:"traits"`
Traits map[string]any `json:"traits"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil
}
for k, v := range payload.Traits {
traits[k] = v
}
maps.Copy(traits, payload.Traits)
return httpResponse(r, http.StatusOK, `{"ok":true}`), nil
}
@@ -187,7 +184,7 @@ func TestUpdateMe_SyncsLocalReadModelFields(t *testing.T) {
app := fiber.New()
app.Put("/api/v1/user/me", h.UpdateMe)
updateBody, _ := json.Marshal(map[string]interface{}{
updateBody, _ := json.Marshal(map[string]any{
"name": "New Name",
"phone": "01087654321",
"department": "New Dept",