Compare commits

...
Author SHA1 Message Date
zhangwenjian 5ecb1e6e4c style💄: run gofmt over the tree
`gofmt -l` listed 26 files. Seventeen of them were missing the newline at the
end of the file; the rest are indentation that used spaces where the file uses
tabs, a handful of call sites written `f(a,b)`, and the doc comment spacing
gofmt has rewritten since 1.19 (`//X` to `// X`).

Nothing here changes behaviour: `go build ./...` and `go vet ./...` are clean
and `go test ./common/...` passes, which is the half of the tree these files
are concentrated in.

Only the files gofmt named are touched, so the diff reads line by line rather
than as a reflow of the whole repository. `gofmt -l` is now empty, which is the
precondition for gating it in CI -- worth doing, but a separate change.
2026-09-18 18:55:19 +08:00
wenjianzhang 9c299805c1 Merge pull request #938 from go-admin-team/fix/cache-control-stray-token
fix🐛: drop the stray token from the Cache-Control header
2026-09-18 14:53:52 +08:00
zhangwenjian 2a900c9876 fix🐛: drop the stray token from the Cache-Control header
NoCache sent `no-cache, no-store, max-age=0, must-revalidate, value`. The
trailing `, value` is not a directive; it is a leftover token that has been on
every response this middleware touches since the file was written. Unknown
directives are ignored, so nothing misbehaved because of it, but it went out on
the wire and read as a mistake to anyone looking.

The assertion added in #937 pins the old value, so it moves with the source:
removing the token from the middleware alone turns TestNoCache red, which is
the whole point of that test and the reason both lines change together here.
2026-09-18 14:47:28 +08:00
wenjianzhang 0008b943a3 Merge pull request #937 from Tuoxie423/test/header-middleware
test✅: add unit tests for NoCache/Options/Secure middleware
2026-09-18 14:41:31 +08:00
拖鞋423 50c74b1f96 test✅: add unit tests for NoCache/Options/Secure middleware 2026-09-17 23:41:21 +08:00
wenjianzhang ae1eef6d4f Merge pull request #936 from go-admin-team/fix/gen-import-accepts-json-body
fix🐛: accept the import table list from a JSON body as well
2026-09-16 20:53:03 +08:00
zhangwenjian d01cdc040f fix🐛: accept the import table list from a JSON body as well
The generator's import reads its comma-separated table list with
c.Request.FormValue("tables"), which on a request declaring itself as JSON reads
the URL query and nothing else. go-admin-ui v3.2.0 began sending that list in
the body, so the handler saw an empty string, asked information_schema for a
table named "", and every import failed with "table name cannot be empty!" —
on a fresh installation that is the first thing the generator is asked to do.

tablesToImport reads the query first and falls back to the body, so a front end
sending either works against this server. It also drops blank entries:
splitting "" yields one empty name rather than nothing at all, which is why the
old code reached a database query at all before failing.

The front end sends the list in the query again on its side; this half is what
lets an installation already running v3.2.0 recover without changing it.
2026-09-16 17:57:51 +08:00
zhangwenjian 92b9af17b7 refactor🎨: name the empty-table-name message once
The string was spelled out at each site that raises it, and once more in the
test file that asserts on it. A test holding its own copy cannot tell the
difference between the handler answering something else and the message having
been reworded: it goes on asserting a string the server no longer sends, and
goes on passing.

The three copies in app/other/models/tools are left alone; they are raised from
a different layer and nothing asserts on them.
2026-09-16 17:57:37 +08:00
zhangwenjian 898e1b023a refactor🎨: share the generator tests' engine and response decoding
newEngine takes the method, path and handler, so a second test file does not
have to restate the sqlite connection, the driver override and its cleanup, the
CustomError middleware and the two context keys. serveJSON does the same for
running one request and decoding the envelope.

Nothing about what is asserted changes; newColumnListEngine and columnListMsg
keep their names and their callers.
2026-09-16 17:57:28 +08:00
31 changed files with 407 additions and 95 deletions
+2 -2
View File
@@ -3,9 +3,9 @@ package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
@@ -145,4 +145,4 @@ func (e SysApi) DeleteSysApi(c *gin.Context) {
return
}
e.OK(req.GetId(), "删除成功")
}
}
+2 -2
View File
@@ -3,9 +3,9 @@ package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
@@ -216,5 +216,5 @@ func (e SysDictData) GetAll(c *gin.Context) {
l = append(l, d)
}
e.OK(l,"查询成功")
e.OK(l, "查询成功")
}
+10 -10
View File
@@ -4,9 +4,9 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
@@ -31,7 +31,7 @@ type SysDictType struct {
// @Security Bearer
func (e SysDictType) GetPage(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeGetPageReq{}
req := dto.SysDictTypeGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
@@ -62,7 +62,7 @@ func (e SysDictType) GetPage(c *gin.Context) {
// @Security Bearer
func (e SysDictType) Get(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeGetReq{}
req := dto.SysDictTypeGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
@@ -82,7 +82,7 @@ func (e SysDictType) Get(c *gin.Context) {
e.OK(object, "查询成功")
}
//Insert 字典类型创建
// Insert 字典类型创建
// @Summary 添加字典类型
// @Description 获取JSON
// @Tags 字典类型
@@ -94,7 +94,7 @@ func (e SysDictType) Get(c *gin.Context) {
// @Security Bearer
func (e SysDictType) Insert(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeInsertReq{}
req := dto.SysDictTypeInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
@@ -109,7 +109,7 @@ func (e SysDictType) Insert(c *gin.Context) {
err = s.Insert(&req)
if err != nil {
e.Logger.Error(err)
e.Error(500, err,fmt.Sprintf(" 创建字典类型失败,详情:%s", err.Error()))
e.Error(500, err, fmt.Sprintf(" 创建字典类型失败,详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
@@ -127,7 +127,7 @@ func (e SysDictType) Insert(c *gin.Context) {
// @Security Bearer
func (e SysDictType) Update(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeUpdateReq{}
req := dto.SysDictTypeUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
@@ -157,7 +157,7 @@ func (e SysDictType) Update(c *gin.Context) {
// @Security Bearer
func (e SysDictType) Delete(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeDeleteReq{}
req := dto.SysDictTypeDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
@@ -189,7 +189,7 @@ func (e SysDictType) Delete(c *gin.Context) {
// @Security Bearer
func (e SysDictType) GetAll(c *gin.Context) {
s := service.SysDictType{}
req :=dto.SysDictTypeGetPageReq{}
req := dto.SysDictTypeGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
@@ -207,4 +207,4 @@ func (e SysDictType) GetAll(c *gin.Context) {
return
}
e.OK(list, "查询成功")
}
}
+4 -4
View File
@@ -29,7 +29,7 @@ type SysLoginLog struct {
// @Security Bearer
func (e SysLoginLog) GetPage(c *gin.Context) {
s := service.SysLoginLog{}
req :=dto.SysLoginLogGetPageReq{}
req := dto.SysLoginLogGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
@@ -60,7 +60,7 @@ func (e SysLoginLog) GetPage(c *gin.Context) {
// @Security Bearer
func (e SysLoginLog) Get(c *gin.Context) {
s := service.SysLoginLog{}
req :=dto.SysLoginLogGetReq{}
req := dto.SysLoginLogGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
@@ -90,7 +90,7 @@ func (e SysLoginLog) Get(c *gin.Context) {
// @Security Bearer
func (e SysLoginLog) Delete(c *gin.Context) {
s := service.SysLoginLog{}
req :=dto.SysLoginLogDeleteReq{}
req := dto.SysLoginLogDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
@@ -107,4 +107,4 @@ func (e SysLoginLog) Delete(c *gin.Context) {
return
}
e.OK(req.GetId(), "删除成功")
}
}
+3 -3
View File
@@ -65,7 +65,7 @@ func (e SysOperaLog) GetPage(c *gin.Context) {
// @Security Bearer
func (e SysOperaLog) Get(c *gin.Context) {
s := new(service.SysOperaLog)
req :=dto.SysOperaLogGetReq{}
req := dto.SysOperaLogGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
@@ -96,7 +96,7 @@ func (e SysOperaLog) Get(c *gin.Context) {
// @Security Bearer
func (e SysOperaLog) Delete(c *gin.Context) {
s := new(service.SysOperaLog)
req :=dto.SysOperaLogDeleteReq{}
req := dto.SysOperaLogDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
@@ -111,7 +111,7 @@ func (e SysOperaLog) Delete(c *gin.Context) {
err = s.Remove(&req)
if err != nil {
e.Logger.Error(err)
e.Error(500,err, fmt.Sprintf("删除失败!错误详情:%s", err.Error()))
e.Error(500, err, fmt.Sprintf("删除失败!错误详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "删除成功")
+8 -8
View File
@@ -2,12 +2,12 @@ package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
@@ -31,7 +31,7 @@ type SysPost struct {
// @Security Bearer
func (e SysPost) GetPage(c *gin.Context) {
s := service.SysPost{}
req :=dto.SysPostPageReq{}
req := dto.SysPostPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
@@ -65,7 +65,7 @@ func (e SysPost) GetPage(c *gin.Context) {
// @Security Bearer
func (e SysPost) Get(c *gin.Context) {
s := service.SysPost{}
req :=dto.SysPostGetReq{}
req := dto.SysPostGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
@@ -99,7 +99,7 @@ func (e SysPost) Get(c *gin.Context) {
// @Security Bearer
func (e SysPost) Insert(c *gin.Context) {
s := service.SysPost{}
req :=dto.SysPostInsertReq{}
req := dto.SysPostInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
@@ -131,7 +131,7 @@ func (e SysPost) Insert(c *gin.Context) {
// @Security Bearer
func (e SysPost) Update(c *gin.Context) {
s := service.SysPost{}
req :=dto.SysPostUpdateReq{}
req := dto.SysPostUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
@@ -163,7 +163,7 @@ func (e SysPost) Update(c *gin.Context) {
// @Security Bearer
func (e SysPost) Delete(c *gin.Context) {
s := service.SysPost{}
req :=dto.SysPostDeleteReq{}
req := dto.SysPostDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
@@ -181,4 +181,4 @@ func (e SysPost) Delete(c *gin.Context) {
return
}
e.OK(req.GetId(), "删除成功")
}
}
+1 -1
View File
@@ -29,4 +29,4 @@ func registerSysDeptRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
r1.GET("/deptTree", api.Get2Tree)
}
}
}
+1 -1
View File
@@ -21,4 +21,4 @@ func registerSysLoginLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
r.GET("/:id", api.Get)
r.DELETE("", api.Delete)
}
}
}
+1 -1
View File
@@ -30,4 +30,4 @@ func registerSysMenuRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
//r1.GET("/menuids", api.GetMenuIDS)
}
}
}
+1 -1
View File
@@ -20,4 +20,4 @@ func registerSysOperaLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
r.GET("/:id", api.Get)
r.DELETE("", api.Delete)
}
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ func registerSyPostRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddlew
r.PUT("/:id", api.Update)
r.DELETE("", api.Delete)
}
}
}
+1 -1
View File
@@ -36,4 +36,4 @@ func registerSysUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
{
v1auth.GET("/getinfo", api.GetInfo)
}
}
}
+9 -9
View File
@@ -7,15 +7,15 @@ import (
// SysDeptGetPageReq 列表或者搜索使用结构体
type SysDeptGetPageReq struct {
DeptId int `form:"deptId" search:"type:exact;column:dept_id;table:sys_dept" comment:"id"` //id
ParentId int `form:"parentId" search:"type:exact;column:parent_id;table:sys_dept" comment:"上级部门"` //上级部门
DeptPath string `form:"deptPath" search:"type:exact;column:dept_path;table:sys_dept" comment:""` //路径
DeptName string `form:"deptName" search:"type:exact;column:dept_name;table:sys_dept" comment:"部门名称"` //部门名称
Sort int `form:"sort" search:"type:exact;column:sort;table:sys_dept" comment:"排序"` //排序
Leader string `form:"leader" search:"type:exact;column:leader;table:sys_dept" comment:"负责人"` //负责人
Phone string `form:"phone" search:"type:exact;column:phone;table:sys_dept" comment:"手机"` //手机
Email string `form:"email" search:"type:exact;column:email;table:sys_dept" comment:"邮箱"` //邮箱
Status string `form:"status" search:"type:exact;column:status;table:sys_dept" comment:"状态"` //状态
DeptId int `form:"deptId" search:"type:exact;column:dept_id;table:sys_dept" comment:"id"` //id
ParentId int `form:"parentId" search:"type:exact;column:parent_id;table:sys_dept" comment:"上级部门"` //上级部门
DeptPath string `form:"deptPath" search:"type:exact;column:dept_path;table:sys_dept" comment:""` //路径
DeptName string `form:"deptName" search:"type:exact;column:dept_name;table:sys_dept" comment:"部门名称"` //部门名称
Sort int `form:"sort" search:"type:exact;column:sort;table:sys_dept" comment:"排序"` //排序
Leader string `form:"leader" search:"type:exact;column:leader;table:sys_dept" comment:"负责人"` //负责人
Phone string `form:"phone" search:"type:exact;column:phone;table:sys_dept" comment:"手机"` //手机
Email string `form:"email" search:"type:exact;column:email;table:sys_dept" comment:"邮箱"` //邮箱
Status string `form:"status" search:"type:exact;column:status;table:sys_dept" comment:"状态"` //状态
}
func (m *SysDeptGetPageReq) GetNeedSearch() interface{} {
+1 -1
View File
@@ -54,4 +54,4 @@ type SysLoginLogDeleteReq struct {
func (s *SysLoginLogDeleteReq) GetId() interface{} {
return s.Ids
}
}
+1 -1
View File
@@ -107,4 +107,4 @@ type SysRoleMenu struct {
// return nil, err
// }
// return r, nil
//}
//}
+7 -1
View File
@@ -8,6 +8,12 @@ import (
"go-admin/app/other/models/tools"
)
// emptyTableNameMsg is what the generator's endpoints answer with when the
// request named no table. Declared once because the tests assert on it: spelled
// out again at each site, a reworded message would leave them asserting on a
// string the server no longer sends, and still passing.
const emptyTableNameMsg = "table name cannot be empty!"
// GetDBColumnList 分页列表数据
// @Summary 分页列表数据 / page list data
// @Description 数据库表列分页列表 / database table column page list
@@ -41,7 +47,7 @@ func (e Gen) GetDBColumnList(c *gin.Context) {
}
data.TableName = c.Request.FormValue("tableName")
pkg.Assert(data.TableName != "", "table name cannot be empty!", 500)
pkg.Assert(data.TableName != "", emptyTableNameMsg, 500)
result, count, err := data.GetPage(db, pageSize, pageIndex)
if err != nil {
log.Errorf("GetPage error, %s", err.Error())
+26 -10
View File
@@ -16,17 +16,21 @@ import (
"go-admin/common/middleware"
)
const emptyTableNameMsg = "table name cannot be empty!"
// bodyOf covers both the success and the CustomError shape: both carry msg.
type bodyOf struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
// newColumnListEngine wires the handler the way the router does, including the
// newEngine wires one generator handler the way the router does, including the
// middleware that turns pkg.Assert's panic into a response.
func newColumnListEngine(t *testing.T) *gin.Engine {
//
// The generator's queries target MySQL's information_schema and cannot run on
// the sqlite connection behind them; the driver setting only has to select that
// branch, since no statement here is expected to succeed. That makes this
// serviceable for any handler in this package whose behaviour is decided before
// the query goes out -- which is what these tests are about.
func newEngine(t *testing.T, method, path string, h gin.HandlerFunc) *gin.Engine {
t.Helper()
gin.SetMode(gin.TestMode)
@@ -35,26 +39,33 @@ func newColumnListEngine(t *testing.T) *gin.Engine {
t.Fatalf("open sqlite: %v", err)
}
// The query targets MySQL's information_schema; the driver setting only has
// to select that branch, the statement itself is never expected to succeed.
previous := config.DatabaseConfig.Driver
config.DatabaseConfig.Driver = "mysql"
t.Cleanup(func() { config.DatabaseConfig.Driver = previous })
r := gin.New()
r.Use(middleware.CustomError)
r.GET("/db/columns/page", func(c *gin.Context) {
r.Handle(method, path, func(c *gin.Context) {
c.Set("db", db)
c.Set(pkg.LoggerKey, logger.NewHelper(logger.DefaultLogger))
Gen{}.GetDBColumnList(c)
h(c)
})
return r
}
func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
func newColumnListEngine(t *testing.T) *gin.Engine {
t.Helper()
return newEngine(t, http.MethodGet, "/db/columns/page", Gen{}.GetDBColumnList)
}
// serveJSON runs one request through the engine and decodes the envelope every
// handler here answers with. A body that will not decode fails the test rather
// than being reported as a mismatched message, which reads as the handler
// having answered something unexpected instead of not having answered at all.
func serveJSON(t *testing.T, r *gin.Engine, req *http.Request) bodyOf {
t.Helper()
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/db/columns/page"+query, nil))
r.ServeHTTP(w, req)
var body bodyOf
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
@@ -63,6 +74,11 @@ func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
return body
}
func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
t.Helper()
return serveJSON(t, r, httptest.NewRequest(http.MethodGet, "/db/columns/page"+query, nil))
}
func TestGetDBColumnList_AcceptsATableName(t *testing.T) {
body := columnListMsg(t, newColumnListEngine(t), "?tableName=sys_user")
if body.Msg == emptyTableNameMsg {
+4 -4
View File
@@ -83,7 +83,7 @@ func (e Gen) Preview(c *gin.Context) {
return
}
tab, _ := table.Get(db,false)
tab, _ := table.Get(db, false)
var b1 bytes.Buffer
err = t1.Execute(&b1, tab)
var b2 bytes.Buffer
@@ -129,7 +129,7 @@ func (e Gen) GenCode(c *gin.Context) {
}
table.TableId = id
tab, _ := table.Get(db,false)
tab, _ := table.Get(db, false)
e.NOActionsGen(c, tab)
@@ -155,7 +155,7 @@ func (e Gen) GenApiToFile(c *gin.Context) {
}
table.TableId = id
tab, _ := table.Get(db,false)
tab, _ := table.Get(db, false)
e.genApiToFile(c, tab)
e.OK("", "Code generated successfully!")
@@ -302,7 +302,7 @@ func (e Gen) GenMenuAndApi(c *gin.Context) {
}
table.TableId = id
tab, _ := table.Get(e.Orm,true)
tab, _ := table.Get(e.Orm, true)
tab.MLTBName = strings.Replace(tab.TBName, "_", "-", -1)
Mmenu := dto.SysMenuInsertReq{}
+52 -5
View File
@@ -1,12 +1,13 @@
package tools
import (
"errors"
"strings"
"github.com/gin-gonic/gin"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"gorm.io/gorm"
"go-admin/app/other/models/tools"
@@ -79,7 +80,7 @@ func (e SysTable) Get(c *gin.Context) {
var data tools.SysTables
data.TableId, _ = pkg.StringToInt(c.Param("tableId"))
result, err := data.Get(db,true)
result, err := data.Get(db, true)
if err != nil {
log.Errorf("Get error, %s", err.Error())
e.Error(500, err, "")
@@ -106,7 +107,7 @@ func (e SysTable) GetSysTablesInfo(c *gin.Context) {
if c.Request.FormValue("tableName") != "" {
data.TBName = c.Request.FormValue("tableName")
}
result, err := data.Get(db,true)
result, err := data.Get(db, true)
if err != nil {
log.Errorf("Get error, %s", err.Error())
e.Error(500, err, "抱歉未找到相关信息")
@@ -148,7 +149,8 @@ func (e SysTable) GetSysTablesTree(c *gin.Context) {
// @Tags 工具 / 生成工具
// @Accept application/json
// @Product application/json
// @Param tables query string false "tableName / 数据表名称"
// @Param tables query string false "tableName / 数据表名称,逗号分隔"
// @Param data body object false "tables / 同上,query 未带时从 JSON body 读"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/sys/tables/info [post]
@@ -163,7 +165,13 @@ func (e SysTable) Insert(c *gin.Context) {
return
}
tablesList := strings.Split(c.Request.FormValue("tables"), ",")
tablesList, err := tablesToImport(c)
if err != nil {
log.Errorf("read the table list, %s", err.Error())
e.Error(500, err, "")
return
}
for i := 0; i < len(tablesList); i++ {
data, err := genTableInit(db, tablesList, i, c)
@@ -184,6 +192,45 @@ func (e SysTable) Insert(c *gin.Context) {
}
// tablesToImport reads the comma-separated table list carried by an import
// request, from the query string or from a JSON body.
//
// The list has only ever travelled in the query string, which is the single
// place FormValue looks once the request declares itself as JSON. A front end
// that puts it in the body instead therefore left this empty, and the import
// went on to ask information_schema for a table named "" -- go-admin-ui v3.2.0
// shipped exactly that, and every import failed with the message below.
// Reading the body when the query has nothing keeps either front end working.
func tablesToImport(c *gin.Context) ([]string, error) {
raw := c.Request.FormValue("tables")
if raw == "" {
var body struct {
Tables string `json:"tables"`
}
// A body that is absent, or shaped some other way, is not itself worth
// reporting: the list is missing either way, and the message below says
// so in the terms the caller asked in.
if err := c.ShouldBindJSON(&body); err == nil {
raw = body.Tables
}
}
parts := strings.Split(raw, ",")
names := make([]string, 0, len(parts))
for _, name := range parts {
// Splitting "" yields one empty name rather than nothing at all, so
// without this an empty list reads as a request to import one table
// whose name happens to be blank.
if name = strings.TrimSpace(name); name != "" {
names = append(names, name)
}
}
if len(names) == 0 {
return nil, errors.New(emptyTableNameMsg)
}
return names, nil
}
func genTableInit(tx *gorm.DB, tablesList []string, i int, c *gin.Context) (tools.SysTables, error) {
var data tools.SysTables
var dbTable tools.DBTables
+124
View File
@@ -0,0 +1,124 @@
package tools
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
// newImportRequest builds the request an import arrives in. Where the table
// list sits -- query or body -- is exactly what these tests are about, and it
// is net/http's form parsing that decides what a handler can reach, so these go
// through a real *http.Request rather than a hand-built one.
func newImportRequest(target, contentType, body string) *http.Request {
req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(body))
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
return req
}
func TestTablesToImport(t *testing.T) {
cases := []struct {
name string
target string
contentType string
body string
want []string
wantErr bool
}{{
name: "from the query, as every front end before v3.2.0 sent it",
target: "/sys/tables/info?tables=sys_user,sys_post",
want: []string{"sys_user", "sys_post"},
}, {
name: "from a JSON body, as go-admin-ui v3.2.0 sends it",
target: "/sys/tables/info",
contentType: "application/json",
body: `{"tables":"sys_user,sys_post"}`,
want: []string{"sys_user", "sys_post"},
}, {
name: "the query wins when a request carries both",
target: "/sys/tables/info?tables=sys_user",
contentType: "application/json",
body: `{"tables":"sys_post"}`,
want: []string{"sys_user"},
}, {
name: "blank entries are dropped rather than imported as a nameless table",
target: "/sys/tables/info?tables=sys_user,,%20,sys_post",
want: []string{"sys_user", "sys_post"},
}, {
name: "a body carrying an empty list is an error",
target: "/sys/tables/info",
contentType: "application/json",
body: `{"tables":""}`,
wantErr: true,
}, {
name: "a body that is not JSON at all is an error, not a panic",
target: "/sys/tables/info",
contentType: "application/json",
body: "sys_user",
wantErr: true,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = newImportRequest(tc.target, tc.contentType, tc.body)
got, err := tablesToImport(c)
if tc.wantErr {
if err == nil {
t.Fatalf("expected an error, got %q", got)
}
if err.Error() != emptyTableNameMsg {
t.Fatalf("message should be the one the front end shows, got %q", err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if strings.Join(got, ",") != strings.Join(tc.want, ",") {
t.Fatalf("got %q, want %q", got, tc.want)
}
})
}
}
// insertMsg runs one import through the wired handler. It asserts nothing about
// the import succeeding -- it cannot, over sqlite -- only about how far the
// request got, which the empty-list message is what distinguishes.
func insertMsg(t *testing.T, target, contentType, body string) bodyOf {
t.Helper()
return serveJSON(t,
newEngine(t, http.MethodPost, "/sys/tables/info", SysTable{}.Insert),
newImportRequest(target, contentType, body))
}
func TestInsert_ReadsTheTableListFromEitherPlace(t *testing.T) {
for _, tc := range []struct {
name string
target string
contentType string
body string
}{
{"query", "/sys/tables/info?tables=sys_user", "", ""},
{"JSON body", "/sys/tables/info", "application/json", `{"tables":"sys_user"}`},
} {
t.Run(tc.name, func(t *testing.T) {
if got := insertMsg(t, tc.target, tc.contentType, tc.body); got.Msg == emptyTableNameMsg {
t.Fatalf("request carried a table name and was still rejected as empty: %+v", got)
}
})
}
}
func TestInsert_RejectsAMissingTableList(t *testing.T) {
if got := insertMsg(t, "/sys/tables/info", "", ""); got.Msg != emptyTableNameMsg {
t.Fatalf("missing table list should be rejected, got %+v", got)
}
}
+1 -1
View File
@@ -5,4 +5,4 @@ import "go-admin/app/demo/router"
func init() {
//注册路由 fixme 其他应用的路由,在本目录新建文件放在init方法
AppRouters = append(AppRouters, router.InitRouter)
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ func init() {
rootCmd.AddCommand(app.StartCmd)
}
//Execute : apply commands
// Execute : apply commands
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(-1)
+1 -1
View File
@@ -13,4 +13,4 @@ type SysApi struct {
func (SysApi) TableName() string {
return "sys_api"
}
}
+18 -18
View File
@@ -1,27 +1,27 @@
package models
type SysMenu struct {
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
MenuName string `json:"menuName" gorm:"size:128;"`
Title string `json:"title" gorm:"size:128;"`
Icon string `json:"icon" gorm:"size:128;"`
Path string `json:"path" gorm:"size:128;"`
Paths string `json:"paths" gorm:"size:128;"`
MenuType string `json:"menuType" gorm:"size:1;"`
Action string `json:"action" gorm:"size:16;"`
Permission string `json:"permission" gorm:"size:255;"`
ParentId int `json:"parentId" gorm:"size:11;"`
NoCache bool `json:"noCache" gorm:"size:8;"`
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
Component string `json:"component" gorm:"size:255;"`
Sort int `json:"sort" gorm:"size:4;"`
Visible string `json:"visible" gorm:"size:1;"`
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
SysApi []SysApi `json:"sysApi" gorm:"many2many:sys_menu_api_rule"`
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
MenuName string `json:"menuName" gorm:"size:128;"`
Title string `json:"title" gorm:"size:128;"`
Icon string `json:"icon" gorm:"size:128;"`
Path string `json:"path" gorm:"size:128;"`
Paths string `json:"paths" gorm:"size:128;"`
MenuType string `json:"menuType" gorm:"size:1;"`
Action string `json:"action" gorm:"size:16;"`
Permission string `json:"permission" gorm:"size:255;"`
ParentId int `json:"parentId" gorm:"size:11;"`
NoCache bool `json:"noCache" gorm:"size:8;"`
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
Component string `json:"component" gorm:"size:255;"`
Sort int `json:"sort" gorm:"size:4;"`
Visible string `json:"visible" gorm:"size:1;"`
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
SysApi []SysApi `json:"sysApi" gorm:"many2many:sys_menu_api_rule"`
ControlBy
ModelTime
}
func (SysMenu) TableName() string {
return "sys_menu"
}
}
+1 -1
View File
@@ -13,4 +13,4 @@ type SysPost struct {
func (SysPost) TableName() string {
return "sys_post"
}
}
+1 -1
View File
@@ -17,4 +17,4 @@ type SysRole struct {
func (SysRole) TableName() string {
return "sys_role"
}
}
+2 -2
View File
@@ -45,8 +45,8 @@ func (e *QiNiuKODO) getToken() (string, error) {
return putPolicy.UploadToken(mac), nil
}
//Setup 装载
//endpoint sss
// Setup 装载
// endpoint sss
func (e *QiNiuKODO) Setup(endpoint, accessKeyID, accessKeySecret, BucketName string, options ...ClientOption) error {
mac := qbox.NewMac(accessKeyID, accessKeySecret)
+2 -2
View File
@@ -10,8 +10,8 @@ type ALiYunOSS struct {
BucketName string
}
//Setup 装载
//endpoint sss
// Setup 装载
// endpoint sss
func (e *ALiYunOSS) Setup(endpoint, accessKeyID, accessKeySecret, BucketName string, options ...ClientOption) error {
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// NoCache is a middleware function that appends headers
// to prevent the client from caching the HTTP response.
func NoCache(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Next()
+119
View File
@@ -0,0 +1,119 @@
package middleware
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
)
func TestNoCache(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
NoCache(c)
if got := w.Header().Get("Cache-Control"); got != "no-cache, no-store, max-age=0, must-revalidate" {
t.Errorf("Cache-Control = %q", got)
}
if got := w.Header().Get("Expires"); got != "Thu, 01 Jan 1970 00:00:00 GMT" {
t.Errorf("Expires = %q", got)
}
if got := w.Header().Get("Last-Modified"); got == "" {
t.Error("Last-Modified should not be empty")
} else if _, err := time.Parse(http.TimeFormat, got); err != nil {
t.Errorf("Last-Modified = %q is not a valid HTTP time: %v", got, err)
}
}
func TestOptions(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("OPTIONS request", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodOptions, "/", nil)
Options(c)
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Errorf("Access-Control-Allow-Origin = %q", got)
}
if got := w.Header().Get("Access-Control-Allow-Methods"); got != "GET,POST,PUT,PATCH,DELETE,OPTIONS" {
t.Errorf("Access-Control-Allow-Methods = %q", got)
}
if got := w.Header().Get("Access-Control-Allow-Headers"); got != "authorization, origin, content-type, accept" {
t.Errorf("Access-Control-Allow-Headers = %q", got)
}
if got := w.Header().Get("Allow"); got != "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS" {
t.Errorf("Allow = %q", got)
}
if got := w.Header().Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type = %q", got)
}
if !c.IsAborted() {
t.Error("expected the request to be aborted")
}
if w.Code != http.StatusOK {
t.Errorf("status code = %d, want %d", w.Code, http.StatusOK)
}
})
t.Run("non-OPTIONS request", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
Options(c)
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("Access-Control-Allow-Origin = %q, want empty", got)
}
if c.IsAborted() {
t.Error("expected the request not to be aborted")
}
})
}
func TestSecure(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Run("without TLS", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
Secure(c)
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Errorf("Access-Control-Allow-Origin = %q", got)
}
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
t.Errorf("X-Content-Type-Options = %q", got)
}
if got := w.Header().Get("X-XSS-Protection"); got != "1; mode=block" {
t.Errorf("X-XSS-Protection = %q", got)
}
if got := w.Header().Get("Strict-Transport-Security"); got != "" {
t.Errorf("Strict-Transport-Security = %q, want empty without TLS", got)
}
})
t.Run("with TLS", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
c.Request.TLS = &tls.ConnectionState{}
Secure(c)
if got := w.Header().Get("Strict-Transport-Security"); got != "max-age=31536000" {
t.Errorf("Strict-Transport-Security = %q", got)
}
})
}
+1 -1
View File
@@ -38,6 +38,6 @@ var CasbinExclude = []UrlInfo{
{Url: "/", Method: "GET"},
{Url: "/api/v1/server-monitor", Method: "GET"},
{Url: "/api/v1/public/uploadFile", Method: "POST"},
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
{Url: "/api/v1/user/pwd/set", Method: "PUT"},
{Url: "/api/v1/sys-user", Method: "PUT"},
}