18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestHandlerRequestParametersUseSnakeCase(t *testing.T) {
|
|
for _, dir := range []string{".", "../api", "../../middleware", "../../pkg/httpx"} {
|
|
fset := token.NewFileSet()
|
|
packages, err := parser.ParseDir(fset, dir, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse handler directory %s: %v", dir, err)
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
for filename, file := range pkg.Files {
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
switch value := node.(type) {
|
|
case *ast.CallExpr:
|
|
checkHandlerParameterCall(t, filename, value)
|
|
case *ast.CompositeLit:
|
|
checkQueryFilterParameter(t, filename, value)
|
|
case *ast.Field:
|
|
checkHandlerRequestTag(t, filename, value)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkHandlerParameterCall(t *testing.T, filename string, call *ast.CallExpr) {
|
|
if identifier, ok := call.Fun.(*ast.Ident); ok {
|
|
if identifier.Name == "requestExternalValue" && len(call.Args) > 2 {
|
|
checkSnakeCaseStringLiteral(t, filename, call.Args[2])
|
|
}
|
|
return
|
|
}
|
|
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return
|
|
}
|
|
identifier, ok := selector.X.(*ast.Ident)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
argIndex := -1
|
|
if identifier.Name == "params" {
|
|
switch selector.Sel.Name {
|
|
case "Get", "GetInt", "GetInt64", "GetBool", "GetTime", "GetInt64Arr", "FormValue":
|
|
argIndex = 1
|
|
}
|
|
} else if identifier.Name == "ctx" {
|
|
switch selector.Sel.Name {
|
|
case "Query", "DefaultQuery", "GetQuery", "QueryArray", "PostForm", "PostFormArray":
|
|
argIndex = 0
|
|
}
|
|
}
|
|
if argIndex < 0 || len(call.Args) <= argIndex {
|
|
return
|
|
}
|
|
checkSnakeCaseStringLiteral(t, filename, call.Args[argIndex])
|
|
}
|
|
|
|
func checkQueryFilterParameter(t *testing.T, filename string, literal *ast.CompositeLit) {
|
|
selector, ok := literal.Type.(*ast.SelectorExpr)
|
|
if !ok || selector.Sel.Name != "QueryFilter" {
|
|
return
|
|
}
|
|
identifier, ok := selector.X.(*ast.Ident)
|
|
if !ok || identifier.Name != "params" {
|
|
return
|
|
}
|
|
for _, element := range literal.Elts {
|
|
pair, ok := element.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := pair.Key.(*ast.Ident)
|
|
if ok && key.Name == "ParamName" {
|
|
checkSnakeCaseStringLiteral(t, filename, pair.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkHandlerRequestTag(t *testing.T, filename string, field *ast.Field) {
|
|
if field.Tag == nil {
|
|
return
|
|
}
|
|
rawTag, err := strconv.Unquote(field.Tag.Value)
|
|
if err != nil {
|
|
t.Errorf("%s: invalid struct tag %s: %v", filename, field.Tag.Value, err)
|
|
return
|
|
}
|
|
for _, key := range []string{"json", "form", "query", "uri"} {
|
|
name := strings.Split(reflect.StructTag(rawTag).Get(key), ",")[0]
|
|
if name != "" && name != "-" && !isSnakeCaseHandlerParameter(name) {
|
|
t.Errorf("%s: %s tag %q must use snake_case", filename, key, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkSnakeCaseStringLiteral(t *testing.T, filename string, expression ast.Expr) {
|
|
literal, ok := expression.(*ast.BasicLit)
|
|
if !ok || literal.Kind != token.STRING {
|
|
return
|
|
}
|
|
name, err := strconv.Unquote(literal.Value)
|
|
if err != nil {
|
|
t.Errorf("%s: invalid request parameter %s: %v", filename, literal.Value, err)
|
|
return
|
|
}
|
|
if !isSnakeCaseHandlerParameter(name) {
|
|
t.Errorf("%s: request parameter %q must use snake_case", filename, name)
|
|
}
|
|
}
|
|
|
|
func isSnakeCaseHandlerParameter(name string) bool {
|
|
for _, r := range name {
|
|
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return name != ""
|
|
}
|