forked from baron/baron-sso
- 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
264 lines
7.6 KiB
Go
264 lines
7.6 KiB
Go
package service
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// clientForHandler returns an http.Client that routes requests to the given handler
|
|
// without real network sockets.
|
|
func clientForHandler(h http.Handler) *http.Client {
|
|
return &http.Client{
|
|
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
// Clone request body for handler
|
|
var bodyBytes []byte
|
|
if req.Body != nil {
|
|
bodyBytes, _ = io.ReadAll(req.Body)
|
|
}
|
|
r := httptest.NewRequest(req.Method, req.URL.String(), bytes.NewReader(bodyBytes))
|
|
r.Header = req.Header.Clone()
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
return w.Result(), nil
|
|
}),
|
|
}
|
|
}
|
|
|
|
type roundTripperFunc func(req *http.Request) (*http.Response, error)
|
|
|
|
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
|
|
|
|
func TestUpdateUserPassword_Success(t *testing.T) {
|
|
const (
|
|
loginID = "user@example.com"
|
|
identityID = "7f0dc8c3-9d5d-4f57-b3d1-123456789abc"
|
|
newPassword = "Sup3rStr0ng!Pass#2026"
|
|
)
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasPrefix(r.URL.Path, "/admin/identities") && r.Method == http.MethodGet:
|
|
if r.URL.Path == "/admin/identities" {
|
|
q := r.URL.Query()
|
|
if got := q.Get("credentials_identifier"); got != loginID {
|
|
t.Fatalf("expected credentials_identifier=%s, got=%s", loginID, got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode([]map[string]any{
|
|
{
|
|
"id": identityID,
|
|
"traits": map[string]any{
|
|
"email": loginID,
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if r.URL.Path != "/admin/identities/"+identityID {
|
|
t.Fatalf("unexpected identity lookup path: %s", r.URL.Path)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": identityID,
|
|
"schema_id": "default",
|
|
"state": "active",
|
|
"traits": map[string]any{
|
|
"email": loginID,
|
|
},
|
|
})
|
|
return
|
|
case r.URL.Path == "/admin/identities/"+identityID && r.Method == http.MethodPut:
|
|
body, _ := io.ReadAll(r.Body)
|
|
if !strings.Contains(string(body), "\"hashed_password\"") {
|
|
t.Fatalf("payload missing hashed_password, body=%s", string(body))
|
|
}
|
|
if strings.Contains(string(body), newPassword) {
|
|
t.Fatalf("payload must not contain plain password, body=%s", string(body))
|
|
}
|
|
if !strings.Contains(string(body), "\"schema_id\":\"default\"") {
|
|
t.Fatalf("payload missing schema_id, body=%s", string(body))
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
default:
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
|
}
|
|
})
|
|
|
|
provider := &OryProvider{
|
|
KratosAdminURL: "http://kratos-admin.local",
|
|
HTTPClient: clientForHandler(handler),
|
|
}
|
|
|
|
if err := provider.UpdateUserPassword(loginID, newPassword, nil); err != nil {
|
|
t.Fatalf("UpdateUserPassword returned error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateUserPassword_NotFound(t *testing.T) {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/admin/identities") && r.Method == http.MethodGet {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
|
})
|
|
|
|
provider := &OryProvider{
|
|
KratosAdminURL: "http://kratos-admin.local",
|
|
HTTPClient: clientForHandler(handler),
|
|
}
|
|
|
|
err := provider.UpdateUserPassword("user@example.com", "Sup3rStr0ng!Pass#2026", nil)
|
|
if err == nil || !strings.Contains(err.Error(), "identity not found") {
|
|
t.Fatalf("expected identity not found error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateUserPassword_ServerError(t *testing.T) {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasPrefix(r.URL.Path, "/admin/identities") && r.Method == http.MethodGet:
|
|
if r.URL.Path == "/admin/identities" {
|
|
_ = json.NewEncoder(w).Encode([]map[string]any{
|
|
{
|
|
"id": "abc",
|
|
"traits": map[string]any{
|
|
"email": "user@example.com",
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if r.URL.Path == "/admin/identities/abc" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "abc",
|
|
"schema_id": "default",
|
|
"state": "active",
|
|
"traits": map[string]any{
|
|
"email": "user@example.com",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
|
case r.URL.Path == "/admin/identities/abc" && r.Method == http.MethodPut:
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
return
|
|
default:
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
|
}
|
|
})
|
|
|
|
provider := &OryProvider{
|
|
KratosAdminURL: "http://kratos-admin.local",
|
|
HTTPClient: clientForHandler(handler),
|
|
}
|
|
|
|
err := provider.UpdateUserPassword("user@example.com", "Sup3rStr0ng!Pass#2026", nil)
|
|
if err == nil || !strings.Contains(err.Error(), "password update failed") {
|
|
t.Fatalf("expected server error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFindIdentityID_QueryEncoding(t *testing.T) {
|
|
loginID := "user+alias@example.com"
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
values, _ := url.ParseQuery(r.URL.RawQuery)
|
|
if values.Get("credentials_identifier") != loginID {
|
|
t.Fatalf("expected credentials_identifier=%s, got=%s", loginID, values.Get("credentials_identifier"))
|
|
}
|
|
_ = json.NewEncoder(w).Encode([]map[string]any{
|
|
{
|
|
"id": "id-123",
|
|
"traits": map[string]any{
|
|
"email": loginID,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
provider := &OryProvider{
|
|
KratosAdminURL: "http://kratos-admin.local",
|
|
HTTPClient: clientForHandler(handler),
|
|
}
|
|
|
|
id, err := provider.findIdentityID(loginID)
|
|
if err != nil {
|
|
t.Fatalf("findIdentityID returned error: %v", err)
|
|
}
|
|
if id != "id-123" {
|
|
t.Fatalf("expected id-123, got %s", id)
|
|
}
|
|
}
|
|
|
|
func TestOryProvider_CreateUser_CustomIDSupport(t *testing.T) {
|
|
const (
|
|
email = "newuser@test.com"
|
|
name = "New User"
|
|
customUuid = "550e8400-e29b-41d4-a716-446655440000"
|
|
password = "secret123456"
|
|
kratosId = "kratos-gen-id"
|
|
)
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/admin/identities" && r.Method == http.MethodGet:
|
|
// No existing identity
|
|
_ = json.NewEncoder(w).Encode([]any{})
|
|
return
|
|
case r.URL.Path == "/admin/identities/"+customUuid && r.Method == http.MethodGet:
|
|
// No identity with this ID
|
|
http.NotFound(w, r)
|
|
return
|
|
case r.URL.Path == "/admin/identities" && r.Method == http.MethodPost:
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
|
|
// Verify ID is NOT in the root to avoid "unknown field id" error
|
|
if _, exists := body["id"]; exists {
|
|
t.Fatalf("payload MUST NOT contain root 'id' field for compatibility")
|
|
}
|
|
|
|
// Verify external_id and metadata_admin
|
|
if got := body["external_id"]; got != customUuid {
|
|
t.Fatalf("expected external_id %s, got %v", customUuid, got)
|
|
}
|
|
meta, ok := body["metadata_admin"].(map[string]any)
|
|
if !ok || meta["original_uuid"] != customUuid {
|
|
t.Fatalf("expected metadata_admin.original_uuid %s, got %v", customUuid, meta)
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": kratosId,
|
|
})
|
|
return
|
|
default:
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
|
}
|
|
})
|
|
|
|
provider := &OryProvider{
|
|
KratosAdminURL: "http://kratos-admin.local",
|
|
HTTPClient: clientForHandler(handler),
|
|
}
|
|
|
|
id, err := provider.CreateUser(&domain.BrokerUser{
|
|
ID: customUuid,
|
|
Email: email,
|
|
Name: name,
|
|
}, password)
|
|
if err != nil {
|
|
t.Fatalf("CreateUser failed: %v", err)
|
|
}
|
|
if id != kratosId {
|
|
t.Fatalf("expected Kratos generated ID %s, got %s", kratosId, id)
|
|
}
|
|
}
|