material_manage_system/internal/handler/routes_test.go
文睿 张 7b926ff60a feat: 物料导入与模板下载接口
- 新增 response.ErrorWithData:校验失败要把逐行明细一起带回,不能只给一句话
- 新增 GET /api/material-import/template 与 POST /api/material-import
- 路由挂在独立前缀 /material-import:materials 组已有 GET /:id,gin 的路由树
  不允许同一段既有静态前缀又有通配参数
- 权限沿用物料新增的口径:登录 + 公司头校验,不额外要求 super_admin
- 模板随代码生成不落盘,下载到的永远与当前校验规则一致
2026-10-10 15:46:57 +08:00

112 lines
3.9 KiB
Go

package handler
import (
"net/http/httptest"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
func TestPhaseOneAndLegacyRoutesAreRegistered(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
NewHandler().RegisterRoutes(router)
registered := make(map[string]bool)
for _, route := range router.Routes() {
registered[route.Method+" "+route.Path] = true
}
want := []string{
"GET /api/auth/organizations", "POST /api/auth/organization-context",
"GET /api/material-categories", "POST /api/material-categories", "PUT /api/material-categories/:id",
"GET /api/units", "POST /api/units", "PUT /api/units/:id",
"GET /api/code-rules", "POST /api/code-rules", "PUT /api/code-rules/:id",
"GET /api/code-rules/next",
"GET /api/organizations", "GET /api/roles",
"PUT /api/roles/:id", "GET /api/permissions",
"GET /api/partners", "POST /api/partners", "PUT /api/partners/:id",
"GET /api/warehouses", "POST /api/warehouses/:id/locations",
"POST /api/materials/:id/obsolete", "POST /api/materials/:id/restore",
"PUT /api/stock/:materialId/warning", "GET /api/stock/:materialId/details",
"POST /api/applications", "GET /api/logs",
"GET /api/material-import/template", "POST /api/material-import",
}
for _, route := range want {
if !registered[route] {
t.Errorf("route not registered: %s", route)
}
}
if registered["DELETE /api/materials/:id"] {
t.Error("hard-delete material route must not be exposed")
}
}
func TestOrganizationBusinessRoutesRequireHeaderBeforeHandler(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
NewHandler().RegisterRoutes(router)
for _, route := range router.Routes() {
path := route.Path
if strings.HasPrefix(path, "/api/auth/") || strings.HasPrefix(path, "/api/users") || strings.HasPrefix(path, "/api/organizations") || strings.HasPrefix(path, "/api/roles") || strings.HasPrefix(path, "/api/permissions") || path == "/api/health" || strings.HasPrefix(path, "/api/upload") {
continue
}
t.Run(route.Method+" "+path, func(t *testing.T) {
mock := contextMock(t)
expectContextPrincipal(mock, "warehouse_admin")
for _, param := range []string{":id", ":materialId", ":locationId"} {
path = strings.ReplaceAll(path, param, "1")
}
req := httptest.NewRequest(route.Method, path, nil)
req.Header.Set("Authorization", "Bearer "+contextToken(t))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 400 || !strings.Contains(w.Body.String(), "X-Organization-ID") {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
})
}
}
func TestOrganizationMasterDataReadAndWritePermissions(t *testing.T) {
for _, tc := range []struct{ path, query string }{
{"/api/material-categories", "SELECT id, organization_id, code, name"},
{"/api/units", "SELECT id, organization_id, code, name"},
{"/api/code-rules", "SELECT cr.id"},
} {
for _, method := range []string{"GET", "POST", "PUT"} {
t.Run(method+tc.path, func(t *testing.T) {
mock := contextMock(t)
expectContextPrincipal(mock, "warehouse_admin")
mock.ExpectQuery("SELECT EXISTS").WithArgs(int64(2), int64(7), int64(7)).WillReturnRows(sqlmock.NewRows([]string{"allowed"}).AddRow(true))
want := 403
if method == "GET" {
want = 200
mock.ExpectQuery(tc.query).WithArgs(int64(2)).WillReturnRows(sqlmock.NewRows([]string{"id"}))
}
path := tc.path
if method == "PUT" {
path += "/1"
}
router := gin.New()
NewHandler().RegisterRoutes(router)
req := httptest.NewRequest(method, path, nil)
req.Header.Set("Authorization", "Bearer "+contextToken(t))
req.Header.Set("X-Organization-ID", "2")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != want {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
})
}
}
}