test: 补路由鉴权、配置回滚与 Agent 版本兼容测试并发布 v1.5.3

This commit is contained in:
2026-09-28 15:53:36 +08:00
parent 0feb6132d6
commit 21efc10536
21 changed files with 570 additions and 58 deletions
+36 -11
View File
@@ -10,7 +10,19 @@ import (
"github.com/fonu/fonu/internal/auth"
)
// routeSpec 是一条 API 路由的注册信息;测试据此遍历全部路由验证鉴权覆盖。
type routeSpec struct {
Pattern string
Handler http.HandlerFunc
Public bool
}
func NewRouter(deps Deps) http.Handler {
handler, _ := newRouterWithSpecs(deps)
return handler
}
func newRouterWithSpecs(deps Deps) (http.Handler, []routeSpec) {
logsHandler := NewLogsHandler(deps.Config)
r := &Router{
authHandler: NewAuthHandler(deps.Auth, deps.Notify),
@@ -33,17 +45,22 @@ func NewRouter(deps Deps) http.Handler {
}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/version", Version(deps.Config))
// 唯一一个匿名只读接口:默认关闭、可设访问码、按 IP 限流
mux.HandleFunc("GET /api/status-page", r.statusPageHandler.Get)
mux.HandleFunc("GET /api/auth/status", r.authHandler.Status)
mux.HandleFunc("POST /api/auth/login", r.authHandler.Login)
mux.HandleFunc("POST /api/auth/logout", r.authHandler.Logout)
protect := func(pattern string, handler http.HandlerFunc) {
mux.Handle(pattern, SessionMiddleware(deps.Auth)(RequireAuth(deps.Auth)(http.HandlerFunc(handler))))
var specs []routeSpec
public := func(pattern string, handler http.HandlerFunc) {
specs = append(specs, routeSpec{Pattern: pattern, Handler: handler, Public: true})
}
protected := func(pattern string, handler http.HandlerFunc) {
specs = append(specs, routeSpec{Pattern: pattern, Handler: handler})
}
public("GET /api/version", Version(deps.Config))
// 唯一一个匿名只读接口:默认关闭、可设访问码、按 IP 限流
public("GET /api/status-page", r.statusPageHandler.Get)
public("GET /api/auth/status", r.authHandler.Status)
public("POST /api/auth/login", r.authHandler.Login)
public("POST /api/auth/logout", r.authHandler.Logout)
protect := protected
protect("POST /api/auth/password", r.authHandler.ChangePassword)
protect("GET /api/settings/tokens", r.authHandler.ListTokens)
@@ -177,11 +194,19 @@ func NewRouter(deps Deps) http.Handler {
mux.Handle("/", r.spaHandler())
}
for _, spec := range specs {
if spec.Public {
mux.HandleFunc(spec.Pattern, spec.Handler)
continue
}
mux.Handle(spec.Pattern, SessionMiddleware(deps.Auth)(RequireAuth(deps.Auth)(spec.Handler)))
}
logger := deps.Logger
if logger == nil {
logger = slog.Default()
}
return loggingMiddleware(logger, SessionMiddleware(deps.Auth)(mux))
return loggingMiddleware(logger, SessionMiddleware(deps.Auth)(mux)), specs
}
type Router struct {
+98
View File
@@ -0,0 +1,98 @@
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/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))
handler, specs := newRouterWithSpecs(Deps{
Config: cfg,
Auth: authSvc,
FRPMulti: frpSvc,
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
}