package api import ( "context" "io" "log/slog" "net/http" "net/http/httptest" "path/filepath" "regexp" "strings" "testing" "github.com/fonu/fonu/internal/auth" "github.com/fonu/fonu/internal/certificate" "github.com/fonu/fonu/internal/cloudflared" "github.com/fonu/fonu/internal/config" "github.com/fonu/fonu/internal/db" "github.com/fonu/fonu/internal/frp" "github.com/fonu/fonu/internal/secret" "github.com/fonu/fonu/internal/settings" ) var routeParamPattern = regexp.MustCompile(`\{[^}]+\}`) // TestProtectedRoutesRequireAuth 遍历路由表:受保护路由匿名必须 401,公开路由不能被鉴权误拦。 func TestProtectedRoutesRequireAuth(t *testing.T) { dir := t.TempDir() conn, err := db.Open(filepath.Join(dir, "fonu.db")) if err != nil { t.Fatalf("open db: %v", err) } defer conn.Close() if err := db.Migrate(context.Background(), conn, filepath.Join("..", "..", "migrations")); err != nil { t.Fatalf("migrate: %v", err) } authSvc := auth.New(conn) box, err := secret.NewBox("router-auth-test-key") if err != nil { t.Fatal(err) } cfg := config.Config{DataDir: dir, NginxPIDFile: filepath.Join(dir, "nginx.pid")} logger := slog.New(slog.NewTextHandler(io.Discard, nil)) frpSvc := frp.NewService(cfg, frp.NewStore(conn), box, logger, certificate.NewStore(conn)) cfSvc := cloudflared.NewService(cfg, cloudflared.NewStore(conn), box, logger, nil) handler, specs := newRouterWithSpecs(Deps{ Config: cfg, Auth: authSvc, FRPMulti: frpSvc, Cloudflare: cfSvc, Settings: settings.NewStore(conn), Logger: logger, }) protected, public := 0, 0 for _, spec := range specs { req := httptest.NewRequest(routeMethod(spec.Pattern), sampleRoutePath(spec.Pattern), nil) code := callRouteSafely(handler, req) if spec.Public { public++ if code == http.StatusUnauthorized { t.Fatalf("公开路由 %s 不应被鉴权拦截", spec.Pattern) } continue } protected++ if code != http.StatusUnauthorized { t.Fatalf("受保护路由 %s 匿名请求应返回 401,实际 %d", spec.Pattern, code) } } if protected < 100 { t.Fatalf("受保护路由覆盖不足:%d 条", protected) } if public < 4 { t.Fatalf("公开路由覆盖不足:%d 条", public) } } func routeMethod(pattern string) string { method, _, _ := strings.Cut(pattern, " ") return method } func sampleRoutePath(pattern string) string { _, path, _ := strings.Cut(pattern, " ") return routeParamPattern.ReplaceAllString(path, "1") } // callRouteSafely 在依赖缺失导致 handler panic 时返回 0,避免公开路由的构造依赖影响鉴权断言。 func callRouteSafely(handler http.Handler, req *http.Request) (code int) { defer func() { if recover() != nil { code = 0 } }() rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) return rec.Code }