package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // --- Mocks --- type MockKratosAdmin struct { mock.Mock } func (m *MockKratosAdmin) ListIdentities(ctx context.Context) ([]service.KratosIdentity, error) { args := m.Called(ctx) return args.Get(0).([]service.KratosIdentity), args.Error(1) } func (m *MockKratosAdmin) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) { args := m.Called(ctx, identifier) return args.String(0), args.Error(1) } func (m *MockKratosAdmin) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*service.KratosIdentity), args.Error(1) } func (m *MockKratosAdmin) UpdateIdentity(ctx context.Context, id string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) { args := m.Called(ctx, id, traits, state) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*service.KratosIdentity), args.Error(1) } func (m *MockKratosAdmin) UpdateIdentityPassword(ctx context.Context, id, pw string) error { return m.Called(ctx, id, pw).Error(0) } func (m *MockKratosAdmin) DeleteIdentity(ctx context.Context, id string) error { return m.Called(ctx, id).Error(0) } func (m *MockKratosAdmin) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) { args := m.Called(ctx, identityID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]service.KratosSession), args.Error(1) } func (m *MockKratosAdmin) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) { args := m.Called(ctx, sessionID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*service.KratosSession), args.Error(1) } func (m *MockKratosAdmin) DeleteSession(ctx context.Context, sessionID string) error { return m.Called(ctx, sessionID).Error(0) } type MockOryProvider struct { mock.Mock } func (m *MockOryProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) { args := m.Called(user, password) return args.String(0), args.Error(1) } func (m *MockOryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error { return m.Called(loginID, newPassword, r).Error(0) } func (m *MockOryProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) { args := m.Called() return args.Get(0).(*domain.PasswordPolicy), args.Error(1) } type MockTenantServiceForUser struct { mock.Mock service.TenantService } func (m *MockTenantServiceForUser) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { args := m.Called(ctx, slug) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Tenant), args.Error(1) } func (m *MockTenantServiceForUser) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Tenant), args.Error(1) } func (m *MockTenantServiceForUser) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Tenant), args.Error(1) } func (m *MockTenantServiceForUser) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) { args := m.Called(ctx, domainName) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Tenant), args.Error(1) } // --- Tests --- func TestUserHandler_BulkCreateUsers(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 - 2 users", func(t *testing.T) { mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: "t-123", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "label": "EmpID", "required": true, "isLoginId": true}, }, }, }, nil).Once() mockTenant.On("GetTenant", mock.Anything, "t-123").Return(&domain.Tenant{ ID: "t-123", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "label": "EmpID", "required": true, "isLoginId": true}, }, }, }, nil) mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil) mockOry.On("CreateUser", mock.Anything, mock.Anything).Return("u-1", nil).Twice() payload := map[string]interface{}{ "users": []map[string]interface{}{ { "email": "user1@test.com", "name": "User One", "tenantSlug": "test-tenant", "metadata": map[string]interface{}{"emp_id": "E001"}, }, { "email": "user2@test.com", "name": "User Two", "tenantSlug": "test-tenant", "metadata": map[string]interface{}{"emp_id": "E002"}, }, }, } 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 map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) results := result["results"].([]interface{}) assert.Len(t, results, 2) assert.True(t, results[0].(map[string]interface{})["success"].(bool)) assert.True(t, results[1].(map[string]interface{})["success"].(bool)) }) t.Run("Fail - Tenant Not Found", func(t *testing.T) { mockTenant.On("GetTenantBySlug", mock.Anything, "wrong-tenant").Return(nil, errors.New("not found")).Once() mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil) payload := map[string]interface{}{ "users": []map[string]interface{}{ { "email": "fail@test.com", "name": "Fail User", "tenantSlug": "wrong-tenant", }, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) results := result["results"].([]interface{}) assert.False(t, results[0].(map[string]interface{})["success"].(bool)) assert.Contains(t, results[0].(map[string]interface{})["message"].(string), "tenant not found") }) t.Run("Fail - Schema Validation (Required)", func(t *testing.T) { mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: "t-123", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "label": "EmpID", "required": true, "isLoginId": true}, }, }, }, nil).Once() mockTenant.On("GetTenant", mock.Anything, "t-123").Return(&domain.Tenant{ ID: "t-123", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "label": "EmpID", "required": true, "isLoginId": true}, }, }, }, nil) payload := map[string]interface{}{ "users": []map[string]interface{}{ { "email": "missing-meta@test.com", "name": "No Meta", "tenantSlug": "test-tenant", "metadata": map[string]interface{}{}, // emp_id missing }, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) results := result["results"].([]interface{}) assert.False(t, results[0].(map[string]interface{})["success"].(bool)) assert.Contains(t, results[0].(map[string]interface{})["message"].(string), "field emp_id is required") }) t.Run("Fail - Schema Validation (Regex)", func(t *testing.T) { mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: "t-123", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "validation": "^E[0-9]{3}$"}, }, }, }, nil).Once() payload := map[string]interface{}{ "users": []map[string]interface{}{ { "email": "regex-fail@test.com", "name": "Regex Fail", "tenantSlug": "test-tenant", "metadata": map[string]interface{}{"emp_id": "abc"}, // Should start with E and 3 digits }, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/users/bulk", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) results := result["results"].([]interface{}) assert.False(t, results[0].(map[string]interface{})["success"].(bool)) assert.Contains(t, results[0].(map[string]interface{})["message"].(string), "match validation pattern") }) } func TestUserHandler_BulkUpdateUsers(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) h := &UserHandler{KratosAdmin: mockKratos} app.Put("/users/bulk", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.BulkUpdateUsers(c) }) t.Run("Success - Update Role and Status", func(t *testing.T) { mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ ID: "u-1", Traits: map[string]interface{}{"email": "u1@test.com"}, State: "active", }, nil).Once() mockKratos.On("UpdateIdentity", mock.Anything, "u-1", mock.Anything, "inactive").Return(&service.KratosIdentity{}, nil).Once() status := "inactive" payload := map[string]interface{}{ "userIds": []string{"u-1"}, "status": &status, } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/bulk", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) results := result["results"].([]interface{}) assert.True(t, results[0].(map[string]interface{})["success"].(bool)) }) } func TestUserHandler_BulkDeleteUsers(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) h := &UserHandler{KratosAdmin: mockKratos} app.Delete("/users/bulk", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.BulkDeleteUsers(c) }) t.Run("Success - Delete multiple", func(t *testing.T) { mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ID: "u-1"}, nil).Once() mockKratos.On("GetIdentity", mock.Anything, "u-2").Return(&service.KratosIdentity{ID: "u-2"}, nil).Once() mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once() mockKratos.On("DeleteIdentity", mock.Anything, "u-2").Return(nil).Once() payload := map[string]interface{}{ "userIds": []string{"u-1", "u-2"}, } body, _ := json.Marshal(payload) req := httptest.NewRequest("DELETE", "/users/bulk", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) }) } func TestUserHandler_UpdateUser_AdminOnlyField(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) mockTenant := new(MockTenantServiceForUser) h := &UserHandler{ KratosAdmin: mockKratos, TenantService: mockTenant, } app.Put("/users/:id", func(c *fiber.Ctx) error { // Mock requester as regular user c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleUser}) return h.UpdateUser(c) }) t.Run("Fail - Regular user updating admin_only field", func(t *testing.T) { mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ ID: "u-1", Traits: map[string]interface{}{"email": "user@test.com", "companyCode": "test-tenant"}, }, nil) mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "salary", "adminOnly": true}, }, }, }, nil) payload := map[string]interface{}{ "metadata": map[string]interface{}{"salary": 5000}, } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/u-1", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 400, resp.StatusCode) // validation failed var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) assert.Contains(t, result["error"].(string), "field salary is admin only") }) } func TestUserHandler_UpdateUser_LoginIDSync(t *testing.T) { t.Run("Success - Sync LoginID from namespaced metadata", func(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) mockTenant := new(MockTenantServiceForUser) h := &UserHandler{ KratosAdmin: mockKratos, TenantService: mockTenant, } app.Put("/users/:id", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.UpdateUser(c) }) tenantID := "t-123" userID := "u-1" mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "email": "user@test.com", "companyCode": "test-tenant", "tenant_id": tenantID, }, }, nil).Once() mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "label": "Employee No", "isLoginId": true}, }, }, }, nil) mockTenant.On("GetTenant", mock.Anything, tenantID).Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "label": "Employee No", "isLoginId": true}, }, }, }, nil) mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once() // Expect traits to include 'custom_login_ids' synced from 'emp_no' mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool { ids, ok := traits["custom_login_ids"].([]string) return ok && len(ids) > 0 && ids[0] == "E1001" }), mock.Anything).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "custom_login_ids": []interface{}{"E1001"}, "email": "user@test.com", }, }, nil).Once() payload := map[string]interface{}{ "metadata": map[string]interface{}{ tenantID: map[string]interface{}{ "emp_no": "E1001", }, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) mockKratos.AssertExpectations(t) }) t.Run("Success - Sync LoginID from existing traits when not in metadata", func(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) mockTenant := new(MockTenantServiceForUser) h := &UserHandler{ KratosAdmin: mockKratos, TenantService: mockTenant, } app.Put("/users/:id", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.UpdateUser(c) }) tenantID := "t-123" userID := "u-2" mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "email": "user2@test.com", "companyCode": "test-tenant", "tenant_id": tenantID, "id": "old-id", tenantID: map[string]interface{}{ "emp_no": "E2002", }, }, }, nil).Once() mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "isLoginId": true}, }, }, }, nil) mockTenant.On("GetTenant", mock.Anything, tenantID).Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "isLoginId": true}, }, }, }, nil) mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once() // Even if metadata is empty, it should sync from existing traits mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool { ids, ok := traits["custom_login_ids"].([]string) return ok && len(ids) > 0 && ids[0] == "E2002" }), mock.Anything).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "custom_login_ids": []interface{}{"E2002"}, }, }, nil).Once() payload := map[string]interface{}{ "name": "New Name", } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) mockKratos.AssertExpectations(t) }) } func TestUserHandler_UpdateUser_PasswordUsesProvider(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) mockOry := new(MockOryProvider) mockTenant := new(MockTenantServiceForUser) h := &UserHandler{ KratosAdmin: mockKratos, OryProvider: mockOry, TenantService: mockTenant, } app.Put("/users/:id", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.UpdateUser(c) }) userID := "u-1" mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "custom_login_ids": []interface{}{"dyddus1210"}, "email": "dyddus1210@gmail.com", "companyCode": "test-tenant", "tenant_id": "t-1", "emp_id": "dyddus1210", }, }, nil).Once() mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: "t-1", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "isLoginId": true}, }, }, }, nil) mockTenant.On("GetTenant", mock.Anything, "t-1").Return(&domain.Tenant{ ID: "t-1", Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_id", "isLoginId": true}, }, }, }, nil) mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once() mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool { ids, ok := traits["custom_login_ids"].([]string) return ok && len(ids) > 0 && ids[0] == "dyddus1210" }), "").Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "custom_login_ids": []interface{}{"dyddus1210"}, "email": "dyddus1210@gmail.com", }, }, nil).Once() mockOry.On("UpdateUserPassword", "dyddus1210", "asdfzxcv1234!", (*http.Request)(nil)).Return(nil).Once() payload := map[string]interface{}{ "password": "asdfzxcv1234!", } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) mockOry.AssertExpectations(t) mockKratos.AssertNotCalled(t, "UpdateIdentityPassword", mock.Anything, mock.Anything, mock.Anything) } func TestUserHandler_UpdateUser_PasswordFallsBackToEmail(t *testing.T) { app := fiber.New() mockKratos := new(MockKratosAdmin) mockOry := new(MockOryProvider) mockTenant := new(MockTenantServiceForUser) h := &UserHandler{ KratosAdmin: mockKratos, OryProvider: mockOry, TenantService: mockTenant, } app.Put("/users/:id", func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin}) return h.UpdateUser(c) }) userID := "u-2" mockKratos.On("GetIdentity", mock.Anything, userID).Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "email": "dyddus1210@gmail.com", "companyCode": "test-tenant", }, }, nil).Once() mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: "t-1", Slug: "test-tenant", }, nil) mockTenant.On("ListManageableTenants", mock.Anything, userID).Return([]domain.Tenant{}, nil).Once() mockKratos.On("UpdateIdentity", mock.Anything, userID, mock.MatchedBy(func(traits map[string]interface{}) bool { return traits["email"] == "dyddus1210@gmail.com" }), "").Return(&service.KratosIdentity{ ID: userID, Traits: map[string]interface{}{ "email": "dyddus1210@gmail.com", }, }, nil).Once() mockOry.On("UpdateUserPassword", "dyddus1210@gmail.com", "asdfzxcv1234!", (*http.Request)(nil)).Return(nil).Once() payload := map[string]interface{}{ "password": "asdfzxcv1234!", } body, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/users/"+userID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 200, resp.StatusCode) mockOry.AssertExpectations(t) } func TestUserHandler_CreateUser_LoginIDSync(t *testing.T) { t.Run("Success - Sync LoginID from namespaced metadata", func(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", h.CreateUser) tenantID := "t-123" mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "label": "Employee No", "isLoginId": true}, }, }, }, nil) mockTenant.On("GetTenant", mock.Anything, tenantID).Return(&domain.Tenant{ ID: tenantID, Slug: "test-tenant", Config: domain.JSONMap{ "userSchema": []interface{}{ map[string]interface{}{"key": "emp_no", "label": "Employee No", "isLoginId": true}, }, }, }, nil) mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil) // Expect OryProvider.CreateUser to be called with attributes["custom_login_ids"] synced from metadata mockOry.On("CreateUser", mock.MatchedBy(func(user *domain.BrokerUser) bool { customIDs, ok := user.Attributes["custom_login_ids"].([]string) return ok && len(customIDs) > 0 && customIDs[0] == "E1001" }), mock.Anything).Return("u-1", nil).Once() // Mock GetIdentity after creation mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ ID: "u-1", Traits: map[string]interface{}{ "custom_login_ids": []interface{}{"E1001"}, "email": "new@test.com", "companyCode": "test-tenant", }, }, nil).Once() // Mock ListManageableTenants for mapIdentitySummary mockTenant.On("ListManageableTenants", mock.Anything, "u-1").Return([]domain.Tenant{}, nil).Once() payload := map[string]interface{}{ "email": "new@test.com", "name": "New User", "companyCode": "test-tenant", "metadata": map[string]interface{}{ tenantID: map[string]interface{}{ "emp_no": "E1001", }, }, } body, _ := json.Marshal(payload) req := httptest.NewRequest("POST", "/users", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, _ := app.Test(req) assert.Equal(t, 201, resp.StatusCode) mockOry.AssertExpectations(t) }) }