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) }) }