test: 覆盖管理端系统安全回归 (#69)

This commit is contained in:
ila
2026-08-26 22:44:52 +08:00
parent 2482ea31c5
commit 2935c8ea5d
2 changed files with 104 additions and 0 deletions
+16
View File
@@ -1,6 +1,8 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
@@ -33,6 +35,20 @@ func TestSystemRouterExposesLoginWithoutCaptcha(t *testing.T) {
t.Fatalf("read-only system route must not be registered: %s", path)
}
}
for _, request := range []struct{ method, path string }{
{http.MethodPost, "/api/v1/menu"},
{http.MethodPut, "/api/v1/menu/1"},
{http.MethodDelete, "/api/v1/menu"},
{http.MethodPut, "/api/v1/sys-api/1"},
{http.MethodDelete, "/api/v1/sys-login-log"},
{http.MethodPut, "/api/v1/roledatascope"},
} {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, httptest.NewRequest(request.method, request.path, nil))
if recorder.Code != http.StatusNotFound {
t.Fatalf("%s %s status = %d, want 404", request.method, request.path, recorder.Code)
}
}
for _, path := range []string{"GET /api/v1/captcha", "GET /api/v1/getCaptcha"} {
if _, ok := routes[path]; ok {
t.Fatalf("captcha route must not be registered: %s", path)
@@ -0,0 +1,88 @@
package service
import (
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
mysqldriver "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"git.ilapage.cn/OPC/chorus/admin/app/admin/models"
"git.ilapage.cn/OPC/chorus/admin/app/admin/service/dto"
"git.ilapage.cn/OPC/chorus/admin/common/actions"
commonDto "git.ilapage.cn/OPC/chorus/admin/common/dto"
"github.com/go-admin-team/go-admin-core/sdk/service"
)
func TestAdminProtectionMySQL(t *testing.T) {
db := openAdminProtectionDB(t)
suffix := fmt.Sprint(time.Now().UnixNano())
usernames := []string{"protection-current-" + suffix, "protection-other-" + suffix}
var role models.SysRole
if err := db.Where("role_key = ?", BuiltInOperatorRoleKey).First(&role).Error; err != nil {
t.Fatalf("load protected role: %v", err)
}
users := []models.SysUser{
{Username: usernames[0], NickName: "Synthetic current", RoleId: role.RoleId, Status: "2"},
{Username: usernames[1], NickName: "Synthetic other", RoleId: role.RoleId, Status: "2"},
}
if err := db.Create(&users).Error; err != nil {
t.Fatalf("create synthetic administrators: %v", err)
}
t.Cleanup(func() {
if err := db.Unscoped().Where("username IN ?", usernames).Delete(&models.SysUser{}).Error; err != nil {
t.Errorf("delete synthetic administrators: %v", err)
}
})
userService := SysUser{Service: service.Service{Orm: db}}
permission := &actions.DataPermission{}
currentStatus := dto.UpdateSysUserStatusReq{UserId: users[0].UserId, Status: "1"}
if err := userService.UpdateStatus(&currentStatus, permission, users[0].UserId); !errors.Is(err, ErrCurrentAdminProtected) {
t.Fatalf("disable current administrator error = %v", err)
}
if err := userService.Remove(&dto.SysUserById{ObjectById: commonDto.ObjectById{Id: users[1].UserId}}, permission, users[0].UserId); err != nil {
t.Fatalf("remove one of two active administrators: %v", err)
}
lastStatus := dto.UpdateSysUserStatusReq{UserId: users[0].UserId, Status: "1"}
if err := userService.UpdateStatus(&lastStatus, permission, 999999); !errors.Is(err, ErrLastAdminProtected) {
t.Fatalf("disable last administrator error = %v", err)
}
roleService := SysRole{Service: service.Service{Orm: db}}
if err := roleService.UpdateStatus(&dto.UpdateStatusReq{RoleId: role.RoleId, Status: "1"}); !errors.Is(err, ErrBuiltInRoleProtected) {
t.Fatalf("disable built-in role error = %v", err)
}
if err := roleService.Update(&dto.SysRoleUpdateReq{RoleId: role.RoleId, RoleName: "Renamed", RoleKey: role.RoleKey, Status: "2"}, nil); !errors.Is(err, ErrBuiltInRoleProtected) {
t.Fatalf("rename built-in role error = %v", err)
}
if err := roleService.Remove(&dto.SysRoleDeleteReq{Ids: []int{role.RoleId}}, nil); !errors.Is(err, ErrBuiltInRoleProtected) {
t.Fatalf("delete built-in role error = %v", err)
}
}
func openAdminProtectionDB(t *testing.T) *gorm.DB {
t.Helper()
if os.Getenv("CHORUS_RUN_ADMIN_TESTS") != "1" {
t.Skip("set CHORUS_RUN_ADMIN_TESTS=1 for the disposable MySQL database")
}
dsn := strings.TrimSpace(os.Getenv("CHORUS_DSN"))
parsed, err := mysqldriver.ParseDSN(dsn)
if err != nil {
t.Fatalf("parse MySQL DSN: %v", err)
}
if parsed.DBName != "chorus_test" {
t.Fatalf("refusing non-disposable database %q", parsed.DBName)
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("open disposable MySQL database: %v", err)
}
return db
}