| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- package web
- import (
- "fmt"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strings"
- "testing"
- "time"
- "github.com/robfig/cron/v3"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/web/global"
- )
- /*
- frontend/src/pages/api-docs/endpoints.ts is a hand-maintained registry: an
- API route omitted there silently vanishes from the generated OpenAPI docs,
- and an entry for a removed route documents an endpoint that 404s. This test
- constructs the real router and diffs it against the registry both ways.
- Scope: everything under /panel/api/ plus the session-auth surface the
- registry also documents (/login, /logout, /csrf-token, /getTwoFactorEnable,
- /ws). SPA page routes are UI, not API, and stay out; registry paths that
- start with "/{" describe the standalone subscription server, which this
- engine does not serve.
- */
- var contractExtraRoutes = map[string]bool{
- "POST /login": true,
- "POST /logout": true,
- "GET /csrf-token": true,
- "POST /getTwoFactorEnable": true,
- "GET /ws": true,
- }
- func inContractScope(method, path string) bool {
- return strings.HasPrefix(path, "/panel/api/") || contractExtraRoutes[method+" "+path]
- }
- func registeredContractRoutes(t *testing.T) map[string]bool {
- t.Helper()
- if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
- t.Fatalf("init db: %v", err)
- }
- t.Cleanup(func() { _ = database.CloseDB() })
- previous := global.GetWebServer()
- s := NewServer()
- s.cron = cron.New(cron.WithLocation(time.Local), cron.WithSeconds())
- global.SetWebServer(s)
- t.Cleanup(func() {
- s.cancel()
- global.SetWebServer(previous)
- })
- engine, err := s.initRouter()
- if err != nil {
- t.Fatalf("init router: %v", err)
- }
- routes := make(map[string]bool)
- for _, r := range engine.Routes() {
- routes[r.Method+" "+r.Path] = true
- }
- if len(routes) == 0 {
- t.Fatal("no routes registered; router construction is broken")
- }
- return routes
- }
- func documentedContractRoutes(t *testing.T) map[string]bool {
- t.Helper()
- source, err := os.ReadFile(filepath.Join("..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts"))
- if err != nil {
- t.Fatalf("read endpoints.ts: %v", err)
- }
- text := string(source)
- methodRe := regexp.MustCompile(`method:\s*'(GET|POST|PUT|DELETE|PATCH|WS)'`)
- pathRe := regexp.MustCompile(`path:\s*'([^']+)'`)
- methods := methodRe.FindAllStringSubmatchIndex(text, -1)
- if declared := strings.Count(text, "method: '"); len(methods) != declared {
- t.Fatalf("parsed %d method fields but endpoints.ts declares %d — the parser regex no longer matches the file shape", len(methods), declared)
- }
- docs := make(map[string]bool)
- for i, m := range methods {
- segmentEnd := len(text)
- if i+1 < len(methods) {
- segmentEnd = methods[i+1][0]
- }
- pathMatch := pathRe.FindStringSubmatch(text[m[1]:segmentEnd])
- if pathMatch == nil {
- t.Fatalf("entry %d in endpoints.ts has a method but no path before the next entry — the parser cannot pair it", i)
- }
- method := text[m[2]:m[3]]
- if strings.HasPrefix(pathMatch[1], "/{") || !strings.HasPrefix(pathMatch[1], "/") {
- continue
- }
- docs[method+" "+pathMatch[1]] = true
- }
- if len(docs) == 0 {
- t.Fatal("no entries parsed from endpoints.ts; the parser regex is broken")
- }
- return docs
- }
- func TestRouteRegistryContract(t *testing.T) {
- registered := registeredContractRoutes(t)
- documented := documentedContractRoutes(t)
- t.Run("every API route is documented", func(t *testing.T) {
- var missing []string
- for route := range registered {
- fields := strings.Fields(route)
- if inContractScope(fields[0], fields[1]) && !documented[route] {
- missing = append(missing, route)
- }
- }
- sort.Strings(missing)
- for _, route := range missing {
- t.Error(fmt.Errorf("route %s is registered but absent from endpoints.ts — add an entry or it vanishes from the API docs", route))
- }
- })
- t.Run("every documented route is registered", func(t *testing.T) {
- var stale []string
- for route := range documented {
- if !registered[route] {
- stale = append(stale, route)
- }
- }
- sort.Strings(stale)
- for _, route := range stale {
- t.Error(fmt.Errorf("endpoints.ts documents %s but the server does not register it — remove or fix the entry", route))
- }
- })
- }
|