Compare commits

...
Author SHA1 Message Date
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
4 changed files with 206 additions and 13 deletions
+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 {
+49 -2
View File
@@ -1,6 +1,7 @@
package tools
import (
"errors"
"strings"
"github.com/gin-gonic/gin"
@@ -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)
}
}