Files
geo/server/internal/shared/swagger/descriptions_parity_test.go
T
root 7d8e82c69f feat(ops): add object storage management and site-mapping CSV import/export
Ship the Ops backstage 对象存储 page (browse / upload / move / delete /
folder ops) backed by a new object-storage service + handler, with the
shared storage client extended with List/Stat/Copy/Usage/DownloadURL so
both MinIO and Aliyun providers cover the new surface. Add
ForcePathStyle to the object-storage config and treat r2/s3/custom_s3
as external providers so Cloudflare R2 and other S3-compatibles can
share the MinIO client path without spinning up the bundled MinIO.

Also add CSV import/export + template download to the site-domain
mappings page, raise gin MaxMultipartMemory to 8 MiB for the new
multipart endpoints, register Ops swagger routes, and extend the
descriptions-parity test to cover the ops router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:19:01 +08:00

154 lines
4.1 KiB
Go

package swagger
import (
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"testing"
)
// TestRouteDocs_CoversEveryRouterEntry parses the tenant-api router source and
// asserts that every registered route has a corresponding entry in routeDocs.
// This is a static-analysis safety net: when a new handler is wired up in
// tenant or ops transport routers, this test fails until a Chinese
// summary is added.
func TestRouteDocs_CoversEveryRouterEntry(t *testing.T) {
routerPaths, bootstrapPath := findRepoSources(t)
routes := parseEngineRoutes(t, bootstrapPath)
for _, routerPath := range routerPaths {
routes = append(routes, parseRoutes(t, routerPath)...)
}
if len(routes) == 0 {
t.Fatalf("could not extract any routes from %s", strings.Join(routerPaths, ", "))
}
missing := make([]string, 0)
for _, r := range routes {
if shouldSkipRoute(r.path) {
continue
}
if _, ok := routeDocs[r.method+" "+r.path]; !ok {
missing = append(missing, r.method+" "+r.path)
}
}
sort.Strings(missing)
if len(missing) > 0 {
t.Fatalf("routeDocs is missing %d route(s):\n %s", len(missing), strings.Join(missing, "\n "))
}
registered := map[string]struct{}{}
for _, r := range routes {
registered[r.method+" "+r.path] = struct{}{}
}
extras := make([]string, 0)
for k := range routeDocs {
if strings.HasPrefix(k, "GET /swagger") {
continue
}
if _, ok := registered[k]; !ok {
extras = append(extras, k)
}
}
sort.Strings(extras)
if len(extras) > 0 {
t.Fatalf("routeDocs has %d entry/entries with no matching route (renamed or removed):\n %s",
len(extras), strings.Join(extras, "\n "))
}
}
type parsedRoute struct {
method string
path string
}
var (
groupRe = regexp.MustCompile(`(\w+)\s*:?=\s*([\w.]+)\.Group\("([^"]*)"\)`)
methodVarRe = regexp.MustCompile(`\b(\w+)\.(GET|POST|PUT|PATCH|DELETE)\("([^"]*)"`)
methodEngRe = regexp.MustCompile(`\b(?:app\.Engine|engine)\.(GET|POST|PUT|PATCH|DELETE)\("([^"]*)"`)
)
func parseRoutes(t *testing.T, path string) []parsedRoute {
t.Helper()
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
text := string(src)
prefixes := map[string][2]string{} // var -> [parent, prefix]
for _, line := range strings.Split(text, "\n") {
if m := groupRe.FindStringSubmatch(line); m != nil {
prefixes[m[1]] = [2]string{m[2], m[3]}
}
}
resolve := func(v string) string {
var parts []string
for {
pp, ok := prefixes[v]
if !ok {
break
}
parts = append([]string{pp[1]}, parts...)
v = pp[0]
}
return strings.Join(parts, "")
}
routes := make([]parsedRoute, 0)
seen := map[string]struct{}{}
add := func(method, fullPath string) {
key := method + " " + fullPath
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
routes = append(routes, parsedRoute{method: method, path: fullPath})
}
for _, line := range strings.Split(text, "\n") {
for _, m := range methodVarRe.FindAllStringSubmatch(line, -1) {
v, meth, p := m[1], m[2], m[3]
if _, ok := prefixes[v]; !ok {
continue
}
add(meth, resolve(v)+p)
}
for _, m := range methodEngRe.FindAllStringSubmatch(line, -1) {
add(m[1], m[2])
}
}
return routes
}
func parseEngineRoutes(t *testing.T, path string) []parsedRoute {
t.Helper()
src, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
routes := make([]parsedRoute, 0)
for _, m := range methodEngRe.FindAllStringSubmatch(string(src), -1) {
routes = append(routes, parsedRoute{method: m[1], path: m[2]})
}
return routes
}
func findRepoSources(t *testing.T) (routers []string, bootstrap string) {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatalf("runtime.Caller failed")
}
// .../server/internal/shared/swagger/descriptions_parity_test.go ->
// .../server is three levels up from this file's parent dir.
serverRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
return []string{
filepath.Join(serverRoot, "internal", "tenant", "transport", "router.go"),
filepath.Join(serverRoot, "internal", "ops", "transport", "router.go"),
},
filepath.Join(serverRoot, "internal", "bootstrap", "bootstrap.go")
}