package handler_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "github.com/gin-gonic/gin" "github.com/user-management-system/internal/api/handler" "github.com/user-management-system/internal/api/middleware" "github.com/user-management-system/internal/api/router" "github.com/user-management-system/internal/auth" "github.com/user-management-system/internal/cache" "github.com/user-management-system/internal/config" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/repository" "github.com/user-management-system/internal/service" gormsqlite "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) var customFieldDbCounter int64 func setupCustomFieldTestServer(t *testing.T) (*httptest.Server, string, string, func()) { t.Helper() gin.SetMode(gin.TestMode) id := atomic.AddInt64(&customFieldDbCounter, 1) dsn := fmt.Sprintf("file:cfdb_%d_%s?mode=memory&cache=shared", id, t.Name()) db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{ DriverName: "sqlite", DSN: dsn, }), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Skipf("skipping custom field test (SQLite unavailable): %v", err) return nil, "", "", func() {} } if err := db.AutoMigrate( &domain.User{}, &domain.Role{}, &domain.Permission{}, &domain.UserRole{}, &domain.RolePermission{}, &domain.CustomField{}, &domain.UserCustomFieldValue{}, ); err != nil { t.Fatalf("db migration failed: %v", err) } seedHandlerAuthzData(t, db) jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{ HS256Secret: "test-cf-secret-key", AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } l1Cache := cache.NewL1Cache() l2Cache := cache.NewRedisCache(false) cacheManager := cache.NewCacheManager(l1Cache, l2Cache) userRepo := repository.NewUserRepository(db) roleRepo := repository.NewRoleRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute) authSvc.SetRoleRepositories(userRoleRepo, roleRepo) fieldRepo := repository.NewCustomFieldRepository(db) valueRepo := repository.NewUserCustomFieldValueRepository(db) cfSvc := service.NewCustomFieldService(fieldRepo, valueRepo) cfHandler := handler.NewCustomFieldHandler(cfSvc) rateLimitCfg := config.RateLimitConfig{} rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg) authMiddleware := middleware.NewAuthMiddleware( jwtManager, userRepo, userRoleRepo, l1Cache, ) authMiddleware.SetCacheManager(cacheManager) authHandler := handler.NewAuthHandler(authSvc) r := router.NewRouter( authHandler, nil, nil, nil, nil, nil, authMiddleware, rateLimitMiddleware, nil, nil, nil, nil, nil, nil, nil, nil, nil, cfHandler, nil, nil, nil, nil, ) engine := r.Setup() server := httptest.NewServer(engine) // Register a regular user regBody := map[string]interface{}{ "username": fmt.Sprintf("cfuser_%d", id), "password": "TestPass123!", "email": fmt.Sprintf("cf_%d@test.com", id), } regBytes, _ := json.Marshal(regBody) regResp, _ := http.Post(server.URL+"/api/v1/auth/register", "application/json", bytes.NewReader(regBytes)) io.ReadAll(regResp.Body) regResp.Body.Close() // Login as regular user loginBody := map[string]interface{}{ "account": regBody["username"], "password": regBody["password"], } loginBytes, _ := json.Marshal(loginBody) loginResp, _ := http.Post(server.URL+"/api/v1/auth/login", "application/json", bytes.NewReader(loginBytes)) var loginResult struct { Data struct { AccessToken string `json:"access_token"` } `json:"data"` } json.NewDecoder(loginResp.Body).Decode(&loginResult) loginResp.Body.Close() userToken := loginResult.Data.AccessToken // Bootstrap admin t.Setenv("BOOTSTRAP_SECRET", fmt.Sprintf("cf-bootstrap-%d", id)) adminToken := bootstrapAdmin(server.URL, fmt.Sprintf("cf-bootstrap-%d", id), fmt.Sprintf("cfadmin_%d", id), fmt.Sprintf("cfa_%d@test.com", id), "AdminPass123!") if adminToken == "" { t.Fatal("bootstrap admin failed") } return server, adminToken, userToken, func() { server.Close() if sqlDB, err := db.DB(); err == nil { sqlDB.Close() } } } func TestCustomFieldHandler_CreateField(t *testing.T) { server, adminToken, userToken, cleanup := setupCustomFieldTestServer(t) defer cleanup() tests := []struct { name string payload map[string]interface{} token string wantStatus int }{ { name: "success", payload: map[string]interface{}{ "name": "Test Field", "field_key": "test_field_create", "type": 1, }, token: adminToken, wantStatus: http.StatusCreated, }, { name: "unauthorized", payload: map[string]interface{}{ "name": "Test Field Unauth", "field_key": "test_field_unauth", "type": 1, }, token: "", wantStatus: http.StatusUnauthorized, }, { name: "forbidden", payload: map[string]interface{}{ "name": "Test Field Forbidden", "field_key": "test_field_forbidden", "type": 1, }, token: userToken, wantStatus: http.StatusForbidden, }, { name: "missing_required_fields", payload: map[string]interface{}{"name": "Missing Key"}, token: adminToken, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doPost(server.URL+"/api/v1/custom-fields", tt.token, tt.payload) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_ListFields(t *testing.T) { server, adminToken, userToken, cleanup := setupCustomFieldTestServer(t) defer cleanup() tests := []struct { name string token string wantStatus int }{ { name: "success_admin", token: adminToken, wantStatus: http.StatusOK, }, { name: "forbidden_regular_user", token: userToken, wantStatus: http.StatusForbidden, }, { name: "unauthorized", token: "", wantStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/custom-fields", tt.token) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_GetField(t *testing.T) { server, adminToken, _, cleanup := setupCustomFieldTestServer(t) defer cleanup() // Create a field createResp, createBody := doPost(server.URL+"/api/v1/custom-fields", adminToken, map[string]interface{}{ "name": "Get Field Test", "field_key": "test_field_get", "type": 1, }) defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { t.Fatalf("create field failed: %d %s", createResp.StatusCode, createBody) } var createResult map[string]interface{} if err := json.Unmarshal([]byte(createBody), &createResult); err != nil { t.Fatalf("parse create response failed: %v", err) } fieldData := createResult["data"].(map[string]interface{}) fieldID := int64(fieldData["id"].(float64)) tests := []struct { name string fieldID string token string wantStatus int }{ { name: "success", fieldID: fmt.Sprintf("%d", fieldID), token: adminToken, wantStatus: http.StatusOK, }, { name: "not_found", fieldID: "99999", token: adminToken, wantStatus: http.StatusNotFound, }, { name: "invalid_id", fieldID: "invalid", token: adminToken, wantStatus: http.StatusBadRequest, }, { name: "unauthorized", fieldID: fmt.Sprintf("%d", fieldID), token: "", wantStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/custom-fields/"+tt.fieldID, tt.token) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_UpdateField(t *testing.T) { server, adminToken, _, cleanup := setupCustomFieldTestServer(t) defer cleanup() // Create a field createResp, createBody := doPost(server.URL+"/api/v1/custom-fields", adminToken, map[string]interface{}{ "name": "Update Field Test", "field_key": "test_field_update", "type": 1, }) defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { t.Fatalf("create field failed: %d %s", createResp.StatusCode, createBody) } var createResult map[string]interface{} if err := json.Unmarshal([]byte(createBody), &createResult); err != nil { t.Fatalf("parse create response failed: %v", err) } fieldData := createResult["data"].(map[string]interface{}) fieldID := int64(fieldData["id"].(float64)) tests := []struct { name string fieldID string payload map[string]interface{} token string wantStatus int }{ { name: "success", fieldID: fmt.Sprintf("%d", fieldID), payload: map[string]interface{}{ "name": "Updated Field Name", }, token: adminToken, wantStatus: http.StatusOK, }, { name: "invalid_id", fieldID: "invalid", payload: map[string]interface{}{ "name": "Updated Field Name", }, token: adminToken, wantStatus: http.StatusBadRequest, }, { name: "unauthorized", fieldID: fmt.Sprintf("%d", fieldID), payload: map[string]interface{}{ "name": "Updated Field Name", }, token: "", wantStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doPut(server.URL+"/api/v1/custom-fields/"+tt.fieldID, tt.token, tt.payload) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_DeleteField(t *testing.T) { server, adminToken, _, cleanup := setupCustomFieldTestServer(t) defer cleanup() // Create a field createResp, createBody := doPost(server.URL+"/api/v1/custom-fields", adminToken, map[string]interface{}{ "name": "Delete Field Test", "field_key": "test_field_delete", "type": 1, }) defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { t.Fatalf("create field failed: %d %s", createResp.StatusCode, createBody) } var createResult map[string]interface{} if err := json.Unmarshal([]byte(createBody), &createResult); err != nil { t.Fatalf("parse create response failed: %v", err) } fieldData := createResult["data"].(map[string]interface{}) fieldID := int64(fieldData["id"].(float64)) tests := []struct { name string fieldID string token string wantStatus int }{ { name: "success", fieldID: fmt.Sprintf("%d", fieldID), token: adminToken, wantStatus: http.StatusOK, }, { name: "invalid_id", fieldID: "invalid", token: adminToken, wantStatus: http.StatusBadRequest, }, { name: "unauthorized", fieldID: fmt.Sprintf("%d", fieldID), token: "", wantStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doDelete(server.URL+"/api/v1/custom-fields/"+tt.fieldID, tt.token) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_SetUserFieldValues(t *testing.T) { server, adminToken, userToken, cleanup := setupCustomFieldTestServer(t) defer cleanup() // Create a field for the user to set createResp, createBody := doPost(server.URL+"/api/v1/custom-fields", adminToken, map[string]interface{}{ "name": "User Field Test", "field_key": "user_field_test", "type": 1, }) defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { t.Fatalf("create field failed: %d %s", createResp.StatusCode, createBody) } tests := []struct { name string payload map[string]interface{} token string wantStatus int }{ { name: "success", payload: map[string]interface{}{ "values": map[string]string{ "user_field_test": "123", }, }, token: userToken, wantStatus: http.StatusOK, }, { name: "unauthorized", payload: map[string]interface{}{ "values": map[string]string{ "user_field_test": "test_value", }, }, token: "", wantStatus: http.StatusUnauthorized, }, { name: "missing_values", payload: map[string]interface{}{}, token: userToken, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doPut(server.URL+"/api/v1/users/me/custom-fields", tt.token, tt.payload) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } } func TestCustomFieldHandler_GetUserFieldValues(t *testing.T) { server, adminToken, userToken, cleanup := setupCustomFieldTestServer(t) defer cleanup() // Create a field createResp, createBody := doPost(server.URL+"/api/v1/custom-fields", adminToken, map[string]interface{}{ "name": "User Field Get Test", "field_key": "user_field_get_test", "type": 1, }) defer createResp.Body.Close() if createResp.StatusCode != http.StatusCreated { t.Fatalf("create field failed: %d %s", createResp.StatusCode, createBody) } // Set a value first setResp, setBody := doPut(server.URL+"/api/v1/users/me/custom-fields", userToken, map[string]interface{}{ "values": map[string]string{ "user_field_get_test": "456", }, }) defer setResp.Body.Close() if setResp.StatusCode != http.StatusOK { t.Fatalf("set field value failed: %d %s", setResp.StatusCode, setBody) } tests := []struct { name string token string wantStatus int }{ { name: "success", token: userToken, wantStatus: http.StatusOK, }, { name: "unauthorized", token: "", wantStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/users/me/custom-fields", tt.token) defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("expected status %d, got %d, body: %s", tt.wantStatus, resp.StatusCode, body) } }) } }