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:
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// --- Mocks ---
|
||||
@@ -113,7 +114,7 @@ func (m *MockKratosAdminService) ListIdentities(ctx context.Context) ([]service.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockKratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
|
||||
func (m *MockKratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]any, state string) (*service.KratosIdentity, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -214,6 +215,8 @@ func (r *passwordLoginUserRepo) FindByCompanyCodes(ctx context.Context, codes []
|
||||
|
||||
func (r *passwordLoginUserRepo) Delete(ctx context.Context, id string) error { return nil }
|
||||
|
||||
func (r *passwordLoginUserRepo) DB() *gorm.DB { return nil }
|
||||
|
||||
func (r *passwordLoginUserRepo) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
|
||||
return nil
|
||||
}
|
||||
@@ -474,7 +477,7 @@ func runHeadlessPasswordLoginWithAssertionRequest(
|
||||
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",
|
||||
@@ -579,7 +582,7 @@ func runHeadlessPasswordLoginWithAssertionAndLoggerRequest(
|
||||
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",
|
||||
@@ -667,7 +670,7 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
|
||||
}
|
||||
json.NewEncoder(w).Encode(domain.HydraLoginRequest{
|
||||
Challenge: challenge,
|
||||
Client: domain.HydraClient{ClientID: "client-1", Metadata: map[string]interface{}{"status": "active"}},
|
||||
Client: domain.HydraClient{ClientID: "client-1", Metadata: map[string]any{"status": "active"}},
|
||||
})
|
||||
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
|
||||
// AcceptLoginRequest
|
||||
@@ -710,7 +713,7 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
json.NewDecoder(resp.Body).Decode(&got)
|
||||
if got["redirectTo"] != "http://rp/cb" {
|
||||
t.Errorf("expected redirectTo http://rp/cb, got %v", got["redirectTo"])
|
||||
@@ -738,7 +741,7 @@ func TestPasswordLogin_OIDC_AuditIncludesClientMetadata(t *testing.T) {
|
||||
Client: domain.HydraClient{
|
||||
ClientID: "devfront",
|
||||
ClientName: "DevFront",
|
||||
Metadata: map[string]interface{}{"status": "active"},
|
||||
Metadata: map[string]any{"status": "active"},
|
||||
},
|
||||
})
|
||||
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
|
||||
@@ -902,7 +905,7 @@ func TestHeadlessPasswordLogin_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",
|
||||
@@ -958,7 +961,7 @@ func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
@@ -1005,7 +1008,7 @@ func TestHeadlessPasswordLogin_OIDCSubjectConflictBlocksMixedRP(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",
|
||||
@@ -1051,7 +1054,7 @@ func TestHeadlessPasswordLogin_OIDCSubjectConflictBlocksMixedRP(t *testing.T) {
|
||||
require.Equal(t, http.StatusConflict, resp.StatusCode)
|
||||
require.False(t, acceptCalled)
|
||||
require.Empty(t, resp.Cookies())
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
require.Equal(t, "oidc_subject_conflict", got["code"])
|
||||
require.Equal(t, "redirect_to_userfront_login", got["recommendedAction"])
|
||||
@@ -1090,7 +1093,7 @@ func TestHeadlessPasswordLogin_OIDCSubjectSameAllowsMixedRP(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",
|
||||
@@ -1134,7 +1137,7 @@ func TestHeadlessPasswordLogin_OIDCSubjectSameAllowsMixedRP(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Empty(t, resp.Cookies())
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
require.Equal(t, "http://rp/cb", got["redirectTo"])
|
||||
require.Nil(t, got["sessionJwt"])
|
||||
@@ -1161,7 +1164,7 @@ func TestHeadlessPasswordLogin_AuditIncludesClientMetadata(t *testing.T) {
|
||||
ClientID: "headless-login-client",
|
||||
ClientName: "Headless Login Portal",
|
||||
TokenEndpointAuthMethod: "none",
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]any{
|
||||
"status": "active",
|
||||
"headless_login_enabled": true,
|
||||
"headless_token_endpoint_auth_method": "private_key_jwt",
|
||||
@@ -1294,7 +1297,7 @@ func TestHeadlessPasswordLogin_IgnoresInlineHeadlessJWKSWhenJWKSURIIsConfigured(
|
||||
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",
|
||||
@@ -1395,7 +1398,7 @@ func TestHeadlessPasswordLogin_RefreshesJWKSWhenSignatureFailsForCachedKid(t *te
|
||||
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",
|
||||
@@ -1494,7 +1497,7 @@ func TestHeadlessPasswordLogin_MissingClientAssertionRejected(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",
|
||||
@@ -1573,7 +1576,7 @@ func TestHeadlessPasswordLogin_InvalidClientAssertionRejected(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",
|
||||
@@ -1995,7 +1998,7 @@ func TestHeadlessPasswordLogin_HeadlessDisabledRejected(t *testing.T) {
|
||||
Client: domain.HydraClient{
|
||||
ClientID: "headless-login-client",
|
||||
TokenEndpointAuthMethod: "none",
|
||||
Metadata: map[string]interface{}{
|
||||
Metadata: map[string]any{
|
||||
"status": "active",
|
||||
"headless_jwks_uri": "https://rp.example.com/.well-known/jwks.json",
|
||||
"headless_token_endpoint_auth_method": "private_key_jwt",
|
||||
@@ -2048,7 +2051,7 @@ func TestHeadlessPasswordLogin_ClientIDMismatchRejected(t *testing.T) {
|
||||
Client: domain.HydraClient{
|
||||
ClientID: "other-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",
|
||||
@@ -2102,7 +2105,7 @@ func TestPasswordLogin_OIDC_InactiveClient(t *testing.T) {
|
||||
hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet {
|
||||
json.NewEncoder(w).Encode(domain.HydraLoginRequest{
|
||||
Client: domain.HydraClient{ClientID: "client-inactive", Metadata: map[string]interface{}{"status": "inactive"}},
|
||||
Client: domain.HydraClient{ClientID: "client-inactive", Metadata: map[string]any{"status": "inactive"}},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -2220,7 +2223,7 @@ func TestPasswordLogin_SharedBrowserSameSubjectAllowed(t *testing.T) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
require.Equal(t, "valid-jwt", got["sessionJwt"])
|
||||
mockIdp.AssertExpectations(t)
|
||||
@@ -2259,7 +2262,7 @@ func TestPasswordLogin_SharedBrowserDifferentSubjectConflicts(t *testing.T) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusConflict, resp.StatusCode)
|
||||
var got map[string]interface{}
|
||||
var got map[string]any
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
require.Equal(t, "session_subject_conflict", got["code"])
|
||||
require.Empty(t, resp.Cookies())
|
||||
|
||||
Reference in New Issue
Block a user