package service import ( "os" "path/filepath" "strings" "testing" "hy2xs-admin/model/constant" ) // Адрес Traffic Stats API — межкомпонентный контракт, и админка обязана // заметить его нарушение, а не работать «как будто там loopback». // // Ломается это одной строкой в `/etc/hysteria/config.yaml`: Hysteria начинает // слушать другой адрес, админка продолжает стучаться в 127.0.0.1, и весь контур // доступа выключается fail-closed — ни один пир не проходит авторизацию, потому // что лимит устройств проверить нечем. Отказ обязан называть причину. func TestParseTrafficStatsPortAcceptsReachableAddresses(t *testing.T) { cases := []struct { name string listen string want int64 }{ {"канонический профиль", "127.0.0.1:36712", 36712}, {"хост не задан", ":36712", 36712}, {"пробелы по краям", " 127.0.0.1:36712 ", 36712}, {"другой адрес loopback", "127.0.0.5:36712", 36712}, // Wildcard включает loopback: обмен состоится, и отказывать в // обслуживании из-за лишней публикации внутреннего API нельзя — // это отключило бы всех пиров. Запрет на wildcard принадлежит // оркестратору, который конфиг и создаёт. {"wildcard", "0.0.0.0:36712", 36712}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := parseTrafficStatsPort(tc.listen) if err != nil { t.Fatalf("адрес %q отклонён: %v", tc.listen, err) } if got != tc.want { t.Fatalf("порт %d, ожидался %d", got, tc.want) } }) } } func TestParseTrafficStatsPortRejectsUnreachableAddresses(t *testing.T) { cases := []struct { name string listen string }{ {"адрес LAN", "192.168.1.10:36712"}, {"публичный адрес", "203.0.113.10:36712"}, {"имя хоста", "hysteria.internal:36712"}, {"пустая строка", ""}, {"порт вне диапазона", "127.0.0.1:70000"}, {"без порта", "127.0.0.1"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if _, err := parseTrafficStatsPort(tc.listen); err == nil { t.Fatalf("недостижимый адрес %q принят", tc.listen) } }) } } // Отказ обязан называть и адрес, и способ починки: оператор, получивший // «connection refused» без объяснения, ищет проблему в Hysteria. func TestGetHysteria2ApiPortExplainsNonLoopbackListen(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") content := "listen: \":443\"\ntrafficStats:\n listen: \"192.168.1.10:36712\"\n secret: \"s\"\n" if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("не удалось записать тестовый конфиг: %v", err) } previous := constant.Hysteria2ConfigPath constant.Hysteria2ConfigPath = path t.Cleanup(func() { constant.Hysteria2ConfigPath = previous }) _, err := GetHysteria2ApiPort() if err == nil { t.Fatal("админка приняла адрес, по которому не может обратиться") } if !strings.Contains(err.Error(), "192.168.1.10") { t.Errorf("сообщение не называет фактический адрес: %q", err.Error()) } if !strings.Contains(err.Error(), "reconfigure") { t.Errorf("сообщение не называет способ починки: %q", err.Error()) } }