This commit is contained in:
ryan
2026-06-08 08:19:22 +08:00
parent 4b72419a96
commit 9d0f9f0576
42 changed files with 1412 additions and 809 deletions
@@ -68,7 +68,7 @@ func TestListAuthSources(t *testing.T) {
}
dbConn.Create(&source)
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
req, _ := http.NewRequest("GET", "/api/v1/admin/auth-sources", nil)
@@ -105,7 +105,7 @@ func TestCreateAuthSource(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("create successfully", func(t *testing.T) {
@@ -175,7 +175,7 @@ func TestUpdateAuthSource(t *testing.T) {
}
dbConn.Create(&source)
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("update keep client secret", func(t *testing.T) {
@@ -252,7 +252,7 @@ func TestToggleAuthSource(t *testing.T) {
}
dbConn.Create(&source)
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("cannot activate without credentials", func(t *testing.T) {
@@ -318,7 +318,7 @@ func TestDeleteAuthSource(t *testing.T) {
}
dbConn.Create(&externalAccount)
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
req, _ := http.NewRequest("DELETE", "/api/v1/admin/auth-sources/1", nil)
@@ -62,7 +62,7 @@ func TestCreateSystemConfig(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("create successfully", func(t *testing.T) {
@@ -124,7 +124,7 @@ func TestListSystemConfigs(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("list all seeded configurations", func(t *testing.T) {
@@ -171,7 +171,7 @@ func TestGetSystemConfig(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("get existing configuration", func(t *testing.T) {
@@ -210,7 +210,7 @@ func TestUpdateSystemConfig(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("update successfully", func(t *testing.T) {
@@ -263,7 +263,7 @@ func TestDeleteSystemConfig(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("delete successfully", func(t *testing.T) {
+6 -6
View File
@@ -61,7 +61,7 @@ func TestListTaskTypes(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/types", nil)
@@ -99,7 +99,7 @@ func TestDispatchTask(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("dispatch valid task successfully", func(t *testing.T) {
@@ -156,7 +156,7 @@ func TestListTaskExecutions(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
ctx := context.Background()
@@ -245,7 +245,7 @@ func TestGetTaskExecution(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
ctx := context.Background()
@@ -308,7 +308,7 @@ func TestRetryTask(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
ctx := context.Background()
@@ -415,7 +415,7 @@ func TestRetryTaskMaxRetryExceeded(t *testing.T) {
_, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
router := setupTestRouter(adminUser)
ctx := context.Background()
+4
View File
@@ -20,4 +20,8 @@ const (
userNotFound = "用户不存在"
cannotDisable = "不能禁用管理员用户"
updateUserFailed = "更新用户状态失败"
usernameExists = "用户名已存在"
usernameRequired = "用户名不能为空"
passwordTooShort = "密码长度不能少于 8 位"
createUserFailed = "创建用户失败"
)
+103 -21
View File
@@ -23,9 +23,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/linux-do/credit/internal/db"
"github.com/linux-do/credit/internal/db/idgen"
"github.com/linux-do/credit/internal/model"
"github.com/linux-do/credit/internal/util"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@@ -38,23 +38,15 @@ type listUsersRequest struct {
}
type user struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
TrustLevel model.TrustLevel `json:"trust_level"`
PayScore int64 `json:"pay_score"`
TotalReceive decimal.Decimal `json:"total_receive"`
TotalPayment decimal.Decimal `json:"total_payment"`
TotalTransfer decimal.Decimal `json:"total_transfer"`
TotalCommunity decimal.Decimal `json:"total_community"`
CommunityBalance decimal.Decimal `json:"community_balance"`
AvailableBalance decimal.Decimal `json:"available_balance"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
LastLoginAt time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
LastLoginAt time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// listUsersResponse 用户列表响应
@@ -76,6 +68,7 @@ type listUsersResponse struct {
// @Failure 403 {object} util.ResponseAny "无管理员权限"
// @Failure 500 {object} util.ResponseAny "内部错误"
// @Router /api/v1/admin/users [get]
// ListUsers 获取用户列表
func ListUsers(c *gin.Context) {
var req listUsersRequest
if err := c.ShouldBindQuery(&req); err != nil {
@@ -105,9 +98,7 @@ func ListUsers(c *gin.Context) {
offset := (req.Page - 1) * req.PageSize
if err := query.
Select("id, username, nickname, avatar_url, trust_level, pay_score, " +
"total_receive, total_payment, total_transfer, total_community, " +
"community_balance, available_balance, is_active, is_admin, " +
Select("id, username, nickname, avatar_url, is_active, is_admin, " +
"last_login_at, created_at, updated_at").
Order("id DESC").
Offset(offset).
@@ -185,3 +176,94 @@ func UpdateUserStatus(c *gin.Context) {
c.JSON(http.StatusOK, util.OKNil())
}
// createUserRequest 创建用户请求
type createUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password" binding:"required,min=8,max=64"`
Nickname string `json:"nickname" binding:"omitempty,max=64"`
IsActive bool `json:"is_active"`
IsAdmin bool `json:"is_admin"`
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建一个本地密码登录的新用户,需要管理员权限
// @Tags admin
// @Accept json
// @Produce json
// @Security SessionCookie
// @Param request body user.createUserRequest true "创建用户参数"
// @Success 200 {object} util.ResponseAny{data=user.user} "创建成功"
// @Failure 400 {object} util.ResponseAny "参数错误或用户名已存在"
// @Failure 401 {object} util.ResponseAny "未登录"
// @Failure 403 {object} util.ResponseAny "无管理员权限"
// @Failure 500 {object} util.ResponseAny "内部错误"
// @Router /api/v1/admin/users [post]
func CreateUser(c *gin.Context) {
var req createUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
return
}
req.Username = strings.TrimSpace(req.Username)
req.Nickname = strings.TrimSpace(req.Nickname)
req.Password = strings.TrimSpace(req.Password)
if req.Username == "" {
c.JSON(http.StatusBadRequest, util.Err(usernameRequired))
return
}
if len(req.Password) < 8 {
c.JSON(http.StatusBadRequest, util.Err(passwordTooShort))
return
}
ctx := c.Request.Context()
var count int64
if err := db.DB(ctx).Table("users").Where("username = ?", req.Username).Count(&count).Error; err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
if count > 0 {
c.JSON(http.StatusBadRequest, util.Err(usernameExists))
return
}
newUser := model.User{
ID: idgen.NextUint64ID(),
Username: req.Username,
Nickname: req.Nickname,
IsActive: req.IsActive,
IsAdmin: req.IsAdmin,
LastLoginAt: time.Time{},
}
if newUser.Nickname == "" {
newUser.Nickname = req.Username
}
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
if err := db.DB(ctx).Create(&newUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
return
}
res := user{
ID: newUser.ID,
Username: newUser.Username,
Nickname: newUser.Nickname,
AvatarUrl: newUser.AvatarUrl,
IsActive: newUser.IsActive,
IsAdmin: newUser.IsAdmin,
LastLoginAt: newUser.LastLoginAt,
CreatedAt: newUser.CreatedAt,
UpdatedAt: newUser.UpdatedAt,
}
c.JSON(http.StatusOK, util.OK(res))
}
+124 -28
View File
@@ -29,7 +29,6 @@ import (
"github.com/linux-do/credit/internal/model"
"github.com/linux-do/credit/internal/testhelper"
"github.com/linux-do/credit/internal/util"
"github.com/shopspring/decimal"
)
func setupTestRouter(authUser *model.User) *gin.Engine {
@@ -46,6 +45,7 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
})
adminGroup.GET("/users", ListUsers)
adminGroup.POST("/users", CreateUser)
adminGroup.PUT("/users/:id/status", UpdateUserStatus)
return r
}
@@ -57,34 +57,28 @@ func TestListUsers(t *testing.T) {
// Seed users
users := []model.User{
{
ID: 1001,
Username: "alice",
Nickname: "Alice Nickname",
IsActive: true,
IsAdmin: false,
AvailableBalance: decimal.NewFromFloat(100.0),
LastLoginAt: time.Now(),
SignKey: "alice_sign_key",
ID: 1001,
Username: "alice",
Nickname: "Alice Nickname",
IsActive: true,
IsAdmin: false,
LastLoginAt: time.Now(),
},
{
ID: 1002,
Username: "bob",
Nickname: "Bob Nickname",
IsActive: true,
IsAdmin: false,
AvailableBalance: decimal.NewFromFloat(50.0),
LastLoginAt: time.Now(),
SignKey: "bob_sign_key",
ID: 1002,
Username: "bob",
Nickname: "Bob Nickname",
IsActive: true,
IsAdmin: false,
LastLoginAt: time.Now(),
},
{
ID: 1003,
Username: "charlie",
Nickname: "Charlie Nickname",
IsActive: false,
IsAdmin: true,
AvailableBalance: decimal.NewFromFloat(9999.0),
LastLoginAt: time.Now(),
SignKey: "charlie_sign_key",
ID: 1003,
Username: "charlie",
Nickname: "Charlie Nickname",
IsActive: false,
IsAdmin: true,
LastLoginAt: time.Now(),
},
}
@@ -94,7 +88,7 @@ func TestListUsers(t *testing.T) {
}
}
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true, SignKey: "charlie_sign_key"}
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("basic pagination list", func(t *testing.T) {
@@ -185,14 +179,12 @@ func TestUpdateUserStatus(t *testing.T) {
Username: "alice",
IsActive: true,
IsAdmin: false,
SignKey: "alice_sign_key",
}
adminUser := model.User{
ID: 1002,
Username: "bob",
IsActive: true,
IsAdmin: true,
SignKey: "bob_sign_key",
}
dbConn.Create(&regularUser)
@@ -252,3 +244,107 @@ func TestUpdateUserStatus(t *testing.T) {
}
})
}
func TestCreateUser(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
router := setupTestRouter(adminUser)
t.Run("create user successfully", func(t *testing.T) {
payload := createUserRequest{
Username: "newuser",
Password: "newpassword123",
Nickname: "New Nickname",
IsActive: true,
IsAdmin: false,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
}
var resp util.ResponseAny
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.ErrorMsg != "" {
t.Errorf("expected empty error message, got '%s'", resp.ErrorMsg)
}
dataBytes, _ := json.Marshal(resp.Data)
var resUser user
if err := json.Unmarshal(dataBytes, &resUser); err != nil {
t.Fatalf("failed to parse response data: %v", err)
}
if resUser.Username != "newuser" || resUser.Nickname != "New Nickname" || !resUser.IsActive || resUser.IsAdmin {
t.Errorf("unexpected user values: %+v", resUser)
}
// Verify in DB
var dbUser model.User
if err := dbConn.Where("username = ?", "newuser").First(&dbUser).Error; err != nil {
t.Fatalf("failed to find user in db: %v", err)
}
if !dbUser.CheckPassword("newpassword123") {
t.Error("password was not hashed correctly")
}
})
t.Run("create user with duplicate username", func(t *testing.T) {
// Create the first user
existing := model.User{
ID: 2001,
Username: "dupuser",
Nickname: "Dup User",
}
dbConn.Create(&existing)
payload := createUserRequest{
Username: "dupuser",
Password: "password123",
Nickname: "Another Nick",
IsActive: true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
}
var resp util.ResponseAny
json.Unmarshal(w.Body.Bytes(), &resp)
if resp.ErrorMsg != usernameExists {
t.Errorf("expected error '%s', got '%s'", usernameExists, resp.ErrorMsg)
}
})
t.Run("validation error - password too short", func(t *testing.T) {
payload := createUserRequest{
Username: "shortpass",
Password: "123",
IsActive: true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
}
})
}