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

@@ -61,12 +61,12 @@ func newKratosWhoamiTestServer(t *testing.T, identityID string) *httptest.Server
http.Error(w, "missing session", http.StatusUnauthorized)
return
}
_ = json.NewEncoder(w).Encode(map[string]interface{}{
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "session-123",
"authenticated_at": "2026-05-21T00:00:00Z",
"identity": map[string]interface{}{
"identity": map[string]any{
"id": identityID,
"traits": map[string]interface{}{
"traits": map[string]any{
"email": "user@example.com",
},
},
@@ -113,7 +113,7 @@ func TestEnchantedLinkFlow_Email_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
var initResp map[string]any
json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
assert.NotEmpty(t, pendingRef)
@@ -129,7 +129,7 @@ func TestEnchantedLinkFlow_Email_Success(t *testing.T) {
assert.NotEmpty(t, token)
// 2. Verify Magic Link
verifyBody, _ := json.Marshal(map[string]interface{}{
verifyBody, _ := json.Marshal(map[string]any{
"token": token,
"verifyOnly": true,
})
@@ -145,7 +145,7 @@ func TestEnchantedLinkFlow_Email_Success(t *testing.T) {
resp, _ = app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var pollResp map[string]interface{}
var pollResp map[string]any
json.NewDecoder(resp.Body).Decode(&pollResp)
assert.Equal(t, "ok", pollResp["status"])
assert.Equal(t, "valid-jwt", pollResp["sessionJwt"])
@@ -177,7 +177,7 @@ func TestEnchantedLinkFlow_Sms_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
var initResp map[string]any
json.NewDecoder(resp.Body).Decode(&initResp)
assert.NotEmpty(t, initResp["userCode"])
}
@@ -193,7 +193,7 @@ func TestVerifyMagicLink_VerifyOnlyWithoutSharedBrowserSessionApprovesOnly(t *te
app := fiber.New()
app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"token": "token-123",
"verifyOnly": true,
})
@@ -204,7 +204,7 @@ func TestVerifyMagicLink_VerifyOnlyWithoutSharedBrowserSessionApprovesOnly(t *te
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "approved", got["status"])
assert.Nil(t, got["sessionJwt"])
@@ -225,7 +225,7 @@ func TestVerifyMagicLink_VerifyOnlySharedBrowserSameSubjectApprovesOnly(t *testi
app := fiber.New()
app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"token": "token-123",
"verifyOnly": true,
})
@@ -237,7 +237,7 @@ func TestVerifyMagicLink_VerifyOnlySharedBrowserSameSubjectApprovesOnly(t *testi
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "approved", got["status"])
assert.Nil(t, got["sessionJwt"])
@@ -258,7 +258,7 @@ func TestVerifyMagicLink_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *
app := fiber.New()
app.Post("/api/v1/auth/magic-link/verify", h.VerifyMagicLink)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"token": "token-123",
"verifyOnly": true,
})
@@ -270,7 +270,7 @@ func TestVerifyMagicLink_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "approved", got["status"])
assert.Nil(t, got["sessionJwt"])
@@ -312,7 +312,7 @@ func TestVerifyLoginCode_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *
app := fiber.New()
app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"loginId": "user@example.com",
"code": "569765",
"pendingRef": "pending-123",
@@ -326,7 +326,7 @@ func TestVerifyLoginCode_VerifyOnlySharedBrowserDifferentSubjectApprovesOnly(t *
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "approved", got["status"])
assert.Nil(t, got["sessionJwt"])
@@ -349,7 +349,7 @@ func TestVerifyLoginCode_MapsSmsPhoneBeforeFlowLookup(t *testing.T) {
app := fiber.New()
app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode)
body, _ := json.Marshal(map[string]interface{}{
body, _ := json.Marshal(map[string]any{
"loginId": "01041585840",
"code": "569765",
"pendingRef": "pending-123",
@@ -360,7 +360,7 @@ func TestVerifyLoginCode_MapsSmsPhoneBeforeFlowLookup(t *testing.T) {
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "approved", got["status"])
assert.Equal(t, "pending-123", got["pendingRef"])
@@ -383,7 +383,7 @@ func TestPollEnchantedLink_ExpiredToken_ReturnsCode(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
var got map[string]interface{}
var got map[string]any
json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "expired_token", got["error"])
assert.Equal(t, "expired_token", got["code"])
@@ -415,7 +415,7 @@ func TestPollEnchantedLink_SharedBrowserSameSubjectIssuesSession(t *testing.T) {
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "ok", got["status"])
assert.Equal(t, "valid-jwt", got["sessionJwt"])
@@ -447,7 +447,7 @@ func TestPollEnchantedLink_SharedBrowserDifferentSubjectConflicts(t *testing.T)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusConflict, resp.StatusCode)
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "session_subject_conflict", got["code"])
assert.NotContains(t, redis.data[prefixSession+"pending-123"], "valid-jwt")
@@ -481,7 +481,7 @@ func TestHeadlessLinkInit_HeadlessLoginClientSuccess(t *testing.T) {
Client: domain.HydraClient{
ClientID: "headless-login-client",
TokenEndpointAuthMethod: "none",
Metadata: map[string]interface{}{
Metadata: map[string]any{
"status": "active",
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
@@ -519,7 +519,7 @@ func TestHeadlessLinkInit_HeadlessLoginClientSuccess(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.NotEmpty(t, got["pendingRef"])
_, hasUserCode := got["userCode"]
@@ -551,7 +551,7 @@ func TestHeadlessLinkPoll_AfterApprovalReturnsRedirect(t *testing.T) {
ClientID: "headless-login-client",
ClientName: "local-demo-rp",
TokenEndpointAuthMethod: "none",
Metadata: map[string]interface{}{
Metadata: map[string]any{
"status": "active",
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
@@ -604,7 +604,7 @@ func TestHeadlessLinkPoll_AfterApprovalReturnsRedirect(t *testing.T) {
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
var initResp map[string]any
_ = json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
assert.NotEmpty(t, pendingRef)
@@ -618,7 +618,7 @@ func TestHeadlessLinkPoll_AfterApprovalReturnsRedirect(t *testing.T) {
}
assert.NotEmpty(t, token)
verifyBody, _ := json.Marshal(map[string]interface{}{
verifyBody, _ := json.Marshal(map[string]any{
"token": token,
"verifyOnly": true,
})
@@ -637,7 +637,7 @@ func TestHeadlessLinkPoll_AfterApprovalReturnsRedirect(t *testing.T) {
resp, _ = app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var pollResp map[string]interface{}
var pollResp map[string]any
_ = json.NewDecoder(resp.Body).Decode(&pollResp)
assert.Equal(t, "http://rp/cb", pollResp["redirectTo"])
assert.Equal(t, "ok", pollResp["status"])
@@ -677,7 +677,7 @@ func TestHeadlessLinkPoll_ApproverSubjectConflictBlocksMixedRP(t *testing.T) {
ClientID: "headless-login-client",
ClientName: "local-demo-rp",
TokenEndpointAuthMethod: "none",
Metadata: map[string]interface{}{
Metadata: map[string]any{
"status": "active",
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
@@ -734,7 +734,7 @@ func TestHeadlessLinkPoll_ApproverSubjectConflictBlocksMixedRP(t *testing.T) {
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
var initResp map[string]any
_ = json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
assert.NotEmpty(t, pendingRef)
@@ -751,7 +751,7 @@ func TestHeadlessLinkPoll_ApproverSubjectConflictBlocksMixedRP(t *testing.T) {
kratosPublic := newKratosWhoamiTestServer(t, "kratos-userfront-a")
t.Setenv("KRATOS_PUBLIC_URL", kratosPublic.URL)
verifyBody, _ := json.Marshal(map[string]interface{}{
verifyBody, _ := json.Marshal(map[string]any{
"token": token,
"verifyOnly": true,
})
@@ -773,7 +773,7 @@ func TestHeadlessLinkPoll_ApproverSubjectConflictBlocksMixedRP(t *testing.T) {
assert.Equal(t, http.StatusConflict, resp.StatusCode)
assert.False(t, acceptCalled)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "oidc_subject_conflict", got["code"])
assert.Equal(t, "redirect_to_userfront_login", got["recommendedAction"])
@@ -802,7 +802,7 @@ func TestHeadlessLinkPoll_RequestCookieSubjectConflictBlocksMixedRP(t *testing.T
Client: domain.HydraClient{
ClientID: "headless-login-client",
TokenEndpointAuthMethod: "none",
Metadata: map[string]interface{}{
Metadata: map[string]any{
"status": "active",
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
@@ -857,7 +857,7 @@ func TestHeadlessLinkPoll_RequestCookieSubjectConflictBlocksMixedRP(t *testing.T
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var initResp map[string]interface{}
var initResp map[string]any
_ = json.NewDecoder(resp.Body).Decode(&initResp)
pendingRef := initResp["pendingRef"].(string)
assert.NotEmpty(t, pendingRef)
@@ -871,7 +871,7 @@ func TestHeadlessLinkPoll_RequestCookieSubjectConflictBlocksMixedRP(t *testing.T
}
assert.NotEmpty(t, token)
verifyBody, _ := json.Marshal(map[string]interface{}{
verifyBody, _ := json.Marshal(map[string]any{
"token": token,
"verifyOnly": true,
})
@@ -896,7 +896,7 @@ func TestHeadlessLinkPoll_RequestCookieSubjectConflictBlocksMixedRP(t *testing.T
assert.Equal(t, http.StatusConflict, resp.StatusCode)
assert.False(t, acceptCalled)
assert.Empty(t, resp.Cookies())
var got map[string]interface{}
var got map[string]any
_ = json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "oidc_subject_conflict", got["code"])
assert.Equal(t, "kratos-userfront-a", got["currentSubject"])