test: 补齐 handler/repository/domain 层单元测试
This commit is contained in:
545
internal/api/handler/custom_field_handler_test.go
Normal file
545
internal/api/handler/custom_field_handler_test.go
Normal file
@@ -0,0 +1,545 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user