From 63dd40a8d70503c3c63f931a0e3ad4bc826a91a3 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Fri, 4 Sep 2026 17:24:41 +0800 Subject: [PATCH] =?UTF-8?q?test=E2=9C=85:=20cover=20the=20data=20scope=20f?= =?UTF-8?q?ailure=20directions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A table over all five scopes plus the ones that are not scopes, asserting the generated SQL rather than a boolean, because the defect was that two different intentions produced the same query. The rows that matter are the negative ones: an unrecognized value, a zero value, and a department scope with a non-positive id. Each was verified to go red with its own fix reverted and the others in place, so a regression names the defect it belongs to. Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx --- common/actions/permission_test.go | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/common/actions/permission_test.go b/common/actions/permission_test.go index 9fef8af2..903d083c 100644 --- a/common/actions/permission_test.go +++ b/common/actions/permission_test.go @@ -6,8 +6,10 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/glebarez/sqlite" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth" "github.com/go-admin-team/go-admin-core/v2/sdk/config" + "gorm.io/gorm" ) // No database is placed in the context on purpose. The middleware needs one @@ -79,3 +81,126 @@ func TestATokenWithoutDeptIdFallsBackToTheQuery(t *testing.T) { t.Fatal("an old token was served from claims it does not have") } } + +// PRD 006 F14/H1. A token without deptid/datascope forces the fallback +// query, which needs pkg.GetOrm(c) - and no "db" key is set in this +// context, so GetOrm fails exactly as it would if a tenant's database were +// unreachable. Before the fix, that error was logged and the handler ran +// anyway with no data permission filter at all. +func TestPermissionActionAbortsWhenDBIsUnavailable(t *testing.T) { + previous := config.ApplicationConfig.EnableDP + config.ApplicationConfig.EnableDP = true + t.Cleanup(func() { config.ApplicationConfig.EnableDP = previous }) + + gin.SetMode(gin.TestMode) + r := gin.New() + handlerReached := false + r.Use(func(c *gin.Context) { + c.Set(jwt.JwtPayloadKey, jwt.MapClaims{"identity": float64(7)}) + }) + r.Use(PermissionAction()) + r.GET("/", func(c *gin.Context) { + handlerReached = true + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if handlerReached { + t.Fatal("the business handler ran with no database and no data permission filter set") + } +} + +// PRD 006 F14/H2 and H3. Table-driven over gorm DryRun so the exact SQL +// Permission produces for each scope is pinned down, not just "some WHERE +// clause got added". +func TestPermissionScopes(t *testing.T) { + previous := config.ApplicationConfig.EnableDP + config.ApplicationConfig.EnableDP = true + t.Cleanup(func() { config.ApplicationConfig.EnableDP = previous }) + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DryRun: true}) + if err != nil { + t.Fatalf("open: %v", err) + } + + const noRows = "SELECT * FROM `t` WHERE 1 = 0" + + cases := []struct { + name string + p *DataPermission + want string + vars []interface{} + }{ + { + name: "all", + p: &DataPermission{DataScope: DataScopeAll}, + want: "SELECT * FROM `t`", + }, + { + name: "custom", + p: &DataPermission{DataScope: DataScopeCustom, RoleId: 3}, + want: "SELECT * FROM `t` WHERE t.create_by in (select sys_user.user_id from sys_role_dept left join sys_user on sys_user.dept_id=sys_role_dept.dept_id where sys_role_dept.role_id = ?)", + vars: []interface{}{3}, + }, + { + name: "dept", + p: &DataPermission{DataScope: DataScopeDept, DeptId: 5}, + want: "SELECT * FROM `t` WHERE t.create_by in (SELECT user_id from sys_user where dept_id = ? )", + vars: []interface{}{5}, + }, + { + name: "dept-tree", + p: &DataPermission{DataScope: DataScopeDeptTree, DeptId: 5}, + want: "SELECT * FROM `t` WHERE t.create_by in (SELECT user_id from sys_user where sys_user.dept_id in(select dept_id from sys_dept where dept_path like ? ))", + vars: []interface{}{"%/5/%"}, + }, + { + name: "self", + p: &DataPermission{DataScope: DataScopeSelf, UserId: 7}, + want: "SELECT * FROM `t` WHERE t.create_by = ?", + vars: []interface{}{7}, + }, + // H2: an unrecognized scope must not read like "all data" any more. + {name: "unrecognized value", p: &DataPermission{DataScope: "6"}, want: noRows}, + // H2/H1: the zero-value DataPermission is what getPermissionFromContext + // and the two "give up and continue" branches in PermissionAction hand + // out when nothing else is available. + {name: "zero value (no scope at all)", p: &DataPermission{}, want: noRows}, + // H3: dept_path always starts with "/0/" (sys_dept.go), so DeptId 0 + // must not be allowed to build a pattern that matches every row. + {name: "dept with DeptId 0", p: &DataPermission{DataScope: DataScopeDept, DeptId: 0}, want: noRows}, + {name: "dept-tree with DeptId 0", p: &DataPermission{DataScope: DataScopeDeptTree, DeptId: 0}, want: noRows}, + {name: "dept-tree with negative DeptId", p: &DataPermission{DataScope: DataScopeDeptTree, DeptId: -1}, want: noRows}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + stmt := db.Session(&gorm.Session{DryRun: true}). + Table("t"). + Scopes(Permission("t", tc.p)). + Find(&[]map[string]interface{}{}). + Statement + + if stmt.SQL.String() != tc.want { + t.Errorf("SQL = %q, want %q", stmt.SQL.String(), tc.want) + } + if tc.vars == nil { + if len(stmt.Vars) != 0 { + t.Errorf("vars = %v, want none", stmt.Vars) + } + return + } + if len(stmt.Vars) != len(tc.vars) { + t.Fatalf("vars = %v, want %v", stmt.Vars, tc.vars) + } + for i := range tc.vars { + if stmt.Vars[i] != tc.vars[i] { + t.Errorf("vars[%d] = %v, want %v", i, stmt.Vars[i], tc.vars[i]) + } + } + }) + } +}