1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/user_handler_uuid_test.go
chan 31d107ff2e 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
2026-06-01 15:34:08 +09:00

258 lines
8.4 KiB
Go

package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"bytes"
"encoding/json"
"fmt"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestUserHandler_BulkCreateUsers_UUIDSupport(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)
t.Run("Success - Provided UUID", func(t *testing.T) {
testUuid := "550e8400-e29b-41d4-a716-446655440000"
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil).Once()
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
// 1. Search-first check: Simulate user NOT found by this UUID
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, testUuid).Return("", nil).Once()
// 2. Create identity
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
return user.ID == testUuid && user.Email == "uuid@test.com"
}), mock.Anything).Return(testUuid, nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"email": "uuid@test.com",
"name": "UUID User",
"tenantSlug": "test-tenant",
"id": testUuid,
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
var result struct {
Results []bulkUserResult `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Len(t, result.Results, 1)
assert.True(t, result.Results[0].Success)
assert.Equal(t, testUuid, result.Results[0].UserID)
})
t.Run("Success - Provided UUID Already Exists (Idempotency)", func(t *testing.T) {
testUuid := "550e8400-e29b-41d4-a716-446655440005"
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil).Once()
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
// 1. Search-first check: Simulate user FOUND by this UUID
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, testUuid).Return(testUuid, nil).Once()
// 2. Fetch existing identity for field comparison
mockKratos.On("GetIdentity", mock.Anything, testUuid).Return(&service.KratosIdentity{
ID: testUuid,
Traits: map[string]any{
"email": "existing@test.com",
"name": "Old Name",
},
}, nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"email": "existing@test.com",
"name": "Existing User",
"tenantSlug": "test-tenant",
"id": testUuid,
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
var result struct {
Results []bulkUserResult `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Len(t, result.Results, 1)
assert.True(t, result.Results[0].Success)
assert.Equal(t, testUuid, result.Results[0].UserID)
assert.Equal(t, "updated", result.Results[0].Status)
// Should have "Name" in modified fields since we changed from "Old Name" to "Existing User"
assert.Contains(t, result.Results[0].ModifiedFields, "Name")
})
t.Run("Fail - Duplicate UUID Conflict", func(t *testing.T) {
testUuid := "550e8400-e29b-41d4-a716-446655440001"
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil).Once()
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
// 1. Search-first check: Simulate NOT found by this UUID (initial check)
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, testUuid).Return("", nil).Once()
// 2. Create identity: Simulate Ory returning a 409 conflict for the UUID
conflictErr := fmt.Errorf("ory provider: identity already exists for uuid=%s", testUuid)
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
return user.ID == testUuid
}), mock.Anything).Return("", conflictErr).Once()
// 3. Conflict double-check: Search by EMAIL to see if it's the same person
// If NOT found by email, it means it's a different person using this UUID
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "conflict@test.com").Return("", nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"email": "conflict@test.com",
"name": "Conflict User",
"tenantSlug": "test-tenant",
"uuid": testUuid,
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
var result struct {
Results []bulkUserResult `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Len(t, result.Results, 1)
assert.False(t, result.Results[0].Success)
assert.Contains(t, result.Results[0].Message, "Conflict: UUID already exists")
})
t.Run("Fail - Invalid UUID Format", func(t *testing.T) {
invalidUuid := "not-a-uuid"
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil).Once()
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
// 1. Search-first check (even for invalid format, it currently tries to search)
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, invalidUuid).Return("", nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"email": "invalid@test.com",
"name": "Invalid User",
"tenantSlug": "test-tenant",
"id": invalidUuid,
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
var result struct {
Results []bulkUserResult `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Len(t, result.Results, 1)
assert.False(t, result.Results[0].Success)
assert.Contains(t, result.Results[0].Message, "invalid UUID format")
})
t.Run("Success - Import after Delete (Visibility Check)", func(t *testing.T) {
// This test is to ensure that if a user is deleted and then re-imported with the same UUID,
// the process succeeds and ideally would result in a visible user.
// NOTE: In this unit test we mock the Repository, so we can't fully test DB behavior,
// but we can verify the Handler logic flow.
testUuid := "550e8400-e29b-41d4-a716-446655440007"
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: "t-1",
Slug: "test-tenant",
}, nil).Once()
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
// 1. Search-first: Simulate NOT found (it was deleted from Kratos)
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, testUuid).Return("", nil).Once()
// 2. Create identity: Succeeds and returns a NEW Kratos ID
newKratosId := "new-kratos-id-after-delete"
mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool {
return user.ID == testUuid
}), mock.Anything).Return(newKratosId, nil).Once()
payload := map[string]any{
"users": []map[string]any{
{
"email": "reimport@test.com",
"name": "Re-import User",
"tenantSlug": "test-tenant",
"id": testUuid,
},
},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req)
assert.Equal(t, 200, resp.StatusCode)
var result struct {
Results []bulkUserResult `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.True(t, result.Results[0].Success)
assert.Equal(t, testUuid, result.Results[0].UserID)
})
}