Files
flvx/go-backend/internal/http/handler/tunnel_probe_target_api_test.go
sagitchu cdcdfd8ff0 fix: use custom probe targets for diagnostics
Move custom probe target controls into the tunnel advanced settings and reuse the configured target in tunnel diagnosis output.
2026-05-02 14:23:51 +08:00

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
}