From 2935c8ea5d20a4e7cf6b686aab2d857ba56a5edc Mon Sep 17 00:00:00 2001 From: ila Date: Wed, 26 Aug 2026 22:44:52 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A6=86=E7=9B=96=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E7=B3=BB=E7=BB=9F=E5=AE=89=E5=85=A8=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=20(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/app/admin/router/sys_router_test.go | 16 ++++ ...admin_protection_mysql_integration_test.go | 88 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 admin/app/admin/service/admin_protection_mysql_integration_test.go diff --git a/admin/app/admin/router/sys_router_test.go b/admin/app/admin/router/sys_router_test.go index a89343b..968ef5d 100644 --- a/admin/app/admin/router/sys_router_test.go +++ b/admin/app/admin/router/sys_router_test.go @@ -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) diff --git a/admin/app/admin/service/admin_protection_mysql_integration_test.go b/admin/app/admin/service/admin_protection_mysql_integration_test.go new file mode 100644 index 0000000..1b37e70 --- /dev/null +++ b/admin/app/admin/service/admin_protection_mysql_integration_test.go @@ -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(¤tStatus, 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 +}