mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
cdcdfd8ff0
Move custom probe target controls into the tunnel advanced settings and reuse the configured target in tunnel diagnosis output.
306 lines
9.5 KiB
Go
306 lines
9.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"go-backend/internal/store/repo"
|
|
)
|
|
|
|
func TestTunnelCreatePersistsProbeTargetAndListReturnsConfiguredValue(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
body := bytes.NewReader([]byte(`{
|
|
"name":"custom-target",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
|
"probeTargetHost":"speed.example.com",
|
|
"probeTargetPort":8443
|
|
}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelCreate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", body))
|
|
assertProbeTargetSuccess(t, res)
|
|
|
|
listRes := httptest.NewRecorder()
|
|
h.tunnelList(listRes, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/list", nil))
|
|
var payload struct {
|
|
Code int `json:"code"`
|
|
Data []map[string]any `json:"data"`
|
|
}
|
|
decodeProbeTargetResponse(t, listRes, &payload)
|
|
if payload.Code != 0 {
|
|
t.Fatalf("expected success, got code %d", payload.Code)
|
|
}
|
|
item := payload.Data[0]
|
|
if item["probeTargetHost"] != "speed.example.com" || item["probeTargetPort"] != float64(8443) {
|
|
t.Fatalf("unexpected probe target in list response: %+v", item)
|
|
}
|
|
}
|
|
|
|
func TestTunnelUpdatePersistsDefaultProbeTargetAsEmpty(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
seedProbeTargetTunnel(t, h, 77, "existing", "old.example.com", 9443)
|
|
body := bytes.NewReader([]byte(`{
|
|
"id":77,
|
|
"name":"existing",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
|
"probeTargetHost":"",
|
|
"probeTargetPort":0
|
|
}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelUpdate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", body))
|
|
assertProbeTargetSuccess(t, res)
|
|
|
|
items, err := h.repo.ListTunnels()
|
|
if err != nil {
|
|
t.Fatalf("list tunnels: %v", err)
|
|
}
|
|
item := findProbeTargetTunnelItem(t, items, 77)
|
|
if item["probeTargetHost"] != "" || item["probeTargetPort"] != 0 {
|
|
t.Fatalf("expected default target to round-trip as empty/0, got %+v", item)
|
|
}
|
|
}
|
|
|
|
func TestTunnelUpdateWithoutProbeTargetFieldsPreservesExistingTarget(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
seedProbeTargetTunnel(t, h, 79, "existing", "old.example.com", 9443)
|
|
body := bytes.NewReader([]byte(`{
|
|
"id":79,
|
|
"name":"existing",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}]
|
|
}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelUpdate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", body))
|
|
assertProbeTargetSuccess(t, res)
|
|
|
|
items, err := h.repo.ListTunnels()
|
|
if err != nil {
|
|
t.Fatalf("list tunnels: %v", err)
|
|
}
|
|
item := findProbeTargetTunnelItem(t, items, 79)
|
|
if item["probeTargetHost"] != "old.example.com" || item["probeTargetPort"] != 9443 {
|
|
t.Fatalf("expected omitted probe target fields to preserve existing target, got %+v", item)
|
|
}
|
|
}
|
|
|
|
func TestTunnelUpdateRejectsInvalidProbeTargetWithoutClearingExistingTarget(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
probeFields string
|
|
}{
|
|
{name: "non numeric port", probeFields: `,"probeTargetPort":"abc"`},
|
|
{name: "fractional port", probeFields: `,"probeTargetPort":443.5`},
|
|
{name: "whitespace host", probeFields: `,"probeTargetHost":" "`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
seedProbeTargetTunnel(t, h, 80, "existing", "old.example.com", 9443)
|
|
body := bytes.NewReader([]byte(`{
|
|
"id":80,
|
|
"name":"existing",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}]
|
|
` + tt.probeFields + `}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelUpdate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", body))
|
|
var payload struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
decodeProbeTargetResponse(t, res, &payload)
|
|
if payload.Code == 0 || payload.Msg == "" {
|
|
t.Fatalf("expected validation failure, got %+v", payload)
|
|
}
|
|
|
|
items, err := h.repo.ListTunnels()
|
|
if err != nil {
|
|
t.Fatalf("list tunnels: %v", err)
|
|
}
|
|
item := findProbeTargetTunnelItem(t, items, 80)
|
|
if item["probeTargetHost"] != "old.example.com" || item["probeTargetPort"] != 9443 {
|
|
t.Fatalf("expected invalid probe target to preserve existing target, got %+v", item)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTunnelCreateRejectsInvalidProbeTarget(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
body := bytes.NewReader([]byte(`{
|
|
"name":"bad-target",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
|
"probeTargetHost":"https://example.com",
|
|
"probeTargetPort":443
|
|
}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelCreate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", body))
|
|
var payload struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
decodeProbeTargetResponse(t, res, &payload)
|
|
if payload.Code == 0 || payload.Msg == "" {
|
|
t.Fatalf("expected validation failure, got %+v", payload)
|
|
}
|
|
}
|
|
|
|
func TestTunnelUpdateInvalidProbeTargetDoesNotCleanFederationBindings(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
seedProbeTargetTunnel(t, h, 88, "existing", "old.example.com", 9443)
|
|
seedProbeTargetFederationBinding(t, h, 88)
|
|
body := bytes.NewReader([]byte(`{
|
|
"id":88,
|
|
"name":"existing",
|
|
"type":1,
|
|
"flow":1,
|
|
"trafficRatio":1,
|
|
"status":1,
|
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
|
"probeTargetHost":"https://example.com",
|
|
"probeTargetPort":443
|
|
}`))
|
|
|
|
res := httptest.NewRecorder()
|
|
h.tunnelUpdate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", body))
|
|
var payload struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
decodeProbeTargetResponse(t, res, &payload)
|
|
if payload.Code == 0 || payload.Msg == "" {
|
|
t.Fatalf("expected validation failure, got %+v", payload)
|
|
}
|
|
|
|
bindings, err := h.repo.ListActiveFederationTunnelBindingsByTunnel(88)
|
|
if err != nil {
|
|
t.Fatalf("list federation bindings: %v", err)
|
|
}
|
|
if len(bindings) != 1 {
|
|
t.Fatalf("expected federation binding to remain after invalid update, got %d", len(bindings))
|
|
}
|
|
}
|
|
|
|
func TestTunnelDiagnosisUsesConfiguredProbeTarget(t *testing.T) {
|
|
h := setupProbeTargetTunnelHandler(t)
|
|
seedProbeTargetTunnel(t, h, 90, "diagnosis-target", "speed.example.com", 8443)
|
|
|
|
_, _, workItems, err := h.prepareTunnelDiagnosis(90)
|
|
if err != nil {
|
|
t.Fatalf("prepare tunnel diagnosis: %v", err)
|
|
}
|
|
if len(workItems) != 1 {
|
|
t.Fatalf("expected one diagnosis item, got %d", len(workItems))
|
|
}
|
|
if workItems[0].targetIP != "speed.example.com" || workItems[0].targetPort != 8443 {
|
|
t.Fatalf("expected custom diagnosis target speed.example.com:8443, got %s:%d", workItems[0].targetIP, workItems[0].targetPort)
|
|
}
|
|
}
|
|
|
|
func setupProbeTargetTunnelHandler(t *testing.T) *Handler {
|
|
t.Helper()
|
|
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = r.Close() })
|
|
h := New(r, "secret")
|
|
now := time.Now().UnixMilli()
|
|
if err := r.DB().Exec(`
|
|
INSERT INTO node(id, name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
|
VALUES(10, 'entry-a', 'entry-secret', '10.0.0.1', '10.0.0.1', '', '30000-30010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0)
|
|
`, now, now).Error; err != nil {
|
|
t.Fatalf("insert node: %v", err)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func seedProbeTargetTunnel(t *testing.T, h *Handler, id int64, name string, host string, port int) {
|
|
t.Helper()
|
|
now := time.Now().UnixMilli()
|
|
if err := h.repo.DB().Exec(`
|
|
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, inx, ip_preference, probe_target_host, probe_target_port)
|
|
VALUES(?, ?, 1, 1, 'tls', 1, ?, ?, 1, ?, '', ?, ?)
|
|
`, id, name, now, now, id, host, port).Error; err != nil {
|
|
t.Fatalf("insert tunnel: %v", err)
|
|
}
|
|
if err := h.repo.DB().Exec(`
|
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
|
VALUES(?, '1', 10, 30001, 'round', 1, 'tls')
|
|
`, id).Error; err != nil {
|
|
t.Fatalf("insert chain: %v", err)
|
|
}
|
|
}
|
|
|
|
func seedProbeTargetFederationBinding(t *testing.T, h *Handler, tunnelID int64) {
|
|
t.Helper()
|
|
now := time.Now().UnixMilli()
|
|
if err := h.repo.DB().Exec(`
|
|
INSERT INTO federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx, remote_url, resource_key, remote_binding_id, allocated_port, status, created_time, updated_time)
|
|
VALUES(?, 10, 1, 0, 'http://peer.example', ?, 'remote-binding', 30001, 1, ?, ?)
|
|
`, tunnelID, "probe-target-test-binding", now, now).Error; err != nil {
|
|
t.Fatalf("insert federation binding: %v", err)
|
|
}
|
|
}
|
|
|
|
func assertProbeTargetSuccess(t *testing.T, res *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
var payload struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
}
|
|
decodeProbeTargetResponse(t, res, &payload)
|
|
if payload.Code != 0 {
|
|
t.Fatalf("expected success, got %+v", payload)
|
|
}
|
|
}
|
|
|
|
func decodeProbeTargetResponse(t *testing.T, res *httptest.ResponseRecorder, v any) {
|
|
t.Helper()
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("expected HTTP %d, got %d", http.StatusOK, res.Code)
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(v); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
}
|
|
|
|
func findProbeTargetTunnelItem(t *testing.T, items []map[string]interface{}, id int64) map[string]interface{} {
|
|
t.Helper()
|
|
for _, item := range items {
|
|
if asInt64(item["id"], 0) == id {
|
|
return item
|
|
}
|
|
}
|
|
t.Fatalf("tunnel %d not found: %+v", id, items)
|
|
return nil
|
|
}
|