package router import ( "strings" "testing" "github.com/gin-gonic/gin" "hy2xs-admin/model/constant" ) // Регистрация маршрутов — единственное место, где ошибка проявляется паникой // при старте сервиса, а не ответом с кодом. Смена пространства имён API как // раз относится к таким изменениям: конфликт с wildcard-маршрутом фронтенда // или дублирующая регистрация обнаружились бы только на живом сервере. func buildRoutes(t *testing.T) gin.RoutesInfo { t.Helper() gin.SetMode(gin.TestMode) engine := gin.New() defer func() { if r := recover(); r != nil { t.Fatalf("сборка маршрутов паникует: %v", r) } }() Router(engine) return engine.Routes() } func TestRouterRegistersMachineAuthUnderInternalNamespace(t *testing.T) { routes := buildRoutes(t) found := false for _, route := range routes { if route.Method == "POST" && route.Path == constant.HysteriaMachineAuthPath { found = true } } if !found { t.Fatalf("machine-auth endpoint не зарегистрирован на %s", constant.HysteriaMachineAuthPath) } } func TestRouterKeepsAdminAndAuthUnderApiNamespace(t *testing.T) { routes := buildRoutes(t) expected := []string{ constant.AdminAPIBase + "/auth/login", constant.AdminAPIBase + "/peers", constant.AdminAPIBase + "/config/listConfig", constant.AdminAPIBase + "/peer-import", constant.AdminAPIBase + "/peer-export", } present := make(map[string]bool, len(routes)) for _, route := range routes { present[route.Path] = true } for _, path := range expected { if !present[path] { t.Errorf("маршрут %s не зарегистрирован", path) } } } // Старое пространство имён — часть runtime-контракта, а не внутреннее имя: // его возвращение сломало бы конфиг Hysteria на уже установленных серверах. func TestRouterHasNoLegacyNamespace(t *testing.T) { for _, route := range buildRoutes(t) { if strings.HasPrefix(route.Path, "/hui") { t.Errorf("вернулось legacy-пространство имён: %s %s", route.Method, route.Path) } } } // Маршруты, операциями которых продукт не владеет, удалены, а не оставлены // отвечающими «feature disabled». func TestRouterHasNoRemovedRoutes(t *testing.T) { removed := []string{ "exportConfig", "importConfig", "updateHysteria2Config", "importHysteria2Config", "hysteria2ChangeVersion", "listRelease", "restartServer", "uploadCertFile", "hysteria2AcmePath", // getConfig принимал произвольный ключ таблицы `config` и был точечным // входом к JWT_SECRET, PEER_SECRET_KEY и PEER_SECRET_ENCRYPTION_KEY. // Потребителей у него не было ни одного. "getConfig", // Алиасы клиентской конфигурации, оставленные «на один миграционный // релиз». Миграционного релиза у clean-install-only продукта нет, а в // 1.0.0 они стали бы частью публичного API. "client-url", "/qr", } for _, route := range buildRoutes(t) { for _, dead := range removed { if strings.Contains(route.Path, dead) { t.Errorf("удалённый маршрут вернулся: %s %s", route.Method, route.Path) } } } } // Оставшиеся маршруты /config — это чтение по allowlist, запись по allowlist и // санитизирующая выгрузка серверного конфига. Никакого generic-доступа к // таблице `config` у API быть не должно. func TestRouterConfigNamespaceIsClosed(t *testing.T) { allowed := map[string]bool{ constant.AdminAPIBase + "/config/updateConfigs": true, constant.AdminAPIBase + "/config/listConfig": true, constant.AdminAPIBase + "/config/getHysteria2Config": true, constant.AdminAPIBase + "/config/exportHysteria2Config": true, } for _, route := range buildRoutes(t) { if !strings.HasPrefix(route.Path, constant.AdminAPIBase+"/config") { continue } if !allowed[route.Path] { t.Errorf("в пространстве /config появился неожиданный маршрут: %s %s", route.Method, route.Path) } } present := make(map[string]bool) for _, route := range buildRoutes(t) { present[route.Path] = true } for path := range allowed { if !present[path] { t.Errorf("маршрут %s пропал", path) } } } func TestRouterExposesHealthz(t *testing.T) { found := false for _, route := range buildRoutes(t) { if route.Method == "GET" && route.Path == "/healthz" { found = true } } if !found { t.Fatal("/healthz не зарегистрирован") } }