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) } 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) } // --- 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}, }, }, }, nil).Once() 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", "companyCode": "test-tenant", "metadata": map[string]interface{}{"emp_id": "E001"}, }, { "email": "user2@test.com", "name": "User Two", "companyCode": "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", "companyCode": "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}, }, }, }, nil).Once() payload := map[string]interface{}{ "users": []map[string]interface{}{ { "email": "missing-meta@test.com", "name": "No Meta", "companyCode": "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", "companyCode": "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_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") }) }