mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-01 00:26:38 +08:00
9b98194a0a
- Add backend API for tunnel delete rule resolution (allow, deny, confirm) - Add contract tests for delete resolution endpoint - Add frontend API types and endpoints for delete resolution - Add tunnel delete resolution settings UI with resolution mode selector - Support per-tunnel and global delete resolution configuration
236 lines
10 KiB
Go
236 lines
10 KiB
Go
package contract_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"go-backend/internal/http/response"
|
|
storeRepo "go-backend/internal/store/repo"
|
|
)
|
|
|
|
func TestTunnelDeletePreviewIncludesDependentRulesContract(t *testing.T) {
|
|
secret := "contract-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
adminToken := mustAdminToken(t, secret)
|
|
now := time.Now().UnixMilli()
|
|
|
|
sourceTunnelID, sourceNodeID := seedTunnelDeleteTunnelWithNode(t, repo, now, "preview-source-tunnel", "preview-source-node", "21000-21010")
|
|
seedTunnelDeleteForward(t, repo, now, sourceTunnelID, sourceNodeID, "preview-forward", 21001)
|
|
|
|
out := requestContractEnvelope(t, router, adminToken, "/api/v1/tunnel/delete-preview", map[string]interface{}{"id": sourceTunnelID})
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected success, got code=%d msg=%q", out.Code, out.Msg)
|
|
}
|
|
|
|
data, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected preview data object, got %T", out.Data)
|
|
}
|
|
if contractValueAsInt64(data["tunnelId"]) != sourceTunnelID {
|
|
t.Fatalf("unexpected tunnelId: %#v", data["tunnelId"])
|
|
}
|
|
if contractValueAsInt64(data["forwardCount"]) != 1 {
|
|
t.Fatalf("expected forwardCount=1, got %#v", data["forwardCount"])
|
|
}
|
|
|
|
samples, ok := data["sampleForwards"].([]interface{})
|
|
if !ok || len(samples) != 1 {
|
|
t.Fatalf("expected one sample forward, got %#v", data["sampleForwards"])
|
|
}
|
|
first, ok := samples[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected sample object, got %T", samples[0])
|
|
}
|
|
if first["name"] != "preview-forward" {
|
|
t.Fatalf("unexpected sample name: %#v", first["name"])
|
|
}
|
|
if contractValueAsInt64(first["inPort"]) != 21001 {
|
|
t.Fatalf("unexpected sample inPort: %#v", first["inPort"])
|
|
}
|
|
}
|
|
|
|
func TestTunnelDeleteWithForwardsDeleteActionRemovesTunnelAndRulesContract(t *testing.T) {
|
|
secret := "contract-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
adminToken := mustAdminToken(t, secret)
|
|
now := time.Now().UnixMilli()
|
|
|
|
sourceTunnelID, sourceNodeID := seedTunnelDeleteTunnelWithNode(t, repo, now, "delete-source-tunnel", "delete-source-node", "22000-22010")
|
|
forwardID := seedTunnelDeleteForward(t, repo, now, sourceTunnelID, sourceNodeID, "delete-forward", 22001)
|
|
|
|
out := requestContractEnvelope(t, router, adminToken, "/api/v1/tunnel/delete-with-forwards", map[string]interface{}{
|
|
"id": sourceTunnelID,
|
|
"action": "delete_forwards",
|
|
})
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected success, got code=%d msg=%q", out.Code, out.Msg)
|
|
}
|
|
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM tunnel WHERE id = ?`, sourceTunnelID); count != 0 {
|
|
t.Fatalf("expected tunnel deleted, got count=%d", count)
|
|
}
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM forward WHERE id = ?`, forwardID); count != 0 {
|
|
t.Fatalf("expected forward deleted, got count=%d", count)
|
|
}
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM forward_port WHERE forward_id = ?`, forwardID); count != 0 {
|
|
t.Fatalf("expected forward ports deleted, got count=%d", count)
|
|
}
|
|
}
|
|
|
|
func TestTunnelDeleteWithForwardsReplaceReturnsFailureDetailsContract(t *testing.T) {
|
|
secret := "contract-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
adminToken := mustAdminToken(t, secret)
|
|
now := time.Now().UnixMilli()
|
|
|
|
sourceTunnelID, sourceNodeID := seedTunnelDeleteTunnelWithNode(t, repo, now, "replace-source-tunnel", "replace-source-node", "23000-23010")
|
|
forwardID := seedTunnelDeleteForward(t, repo, now, sourceTunnelID, sourceNodeID, "replace-forward", 23001)
|
|
targetTunnelID, targetNodeID := seedTunnelDeleteTunnelWithNode(t, repo, now, "replace-target-tunnel", "replace-target-node", "23000-23010")
|
|
seedTunnelDeleteForward(t, repo, now, targetTunnelID, targetNodeID, "occupied-forward", 23001)
|
|
|
|
out := requestContractEnvelope(t, router, adminToken, "/api/v1/tunnel/delete-with-forwards", map[string]interface{}{
|
|
"id": sourceTunnelID,
|
|
"action": "replace",
|
|
"targetTunnelId": targetTunnelID,
|
|
})
|
|
if out.Code != -2 {
|
|
t.Fatalf("expected failure code -2, got code=%d msg=%q", out.Code, out.Msg)
|
|
}
|
|
|
|
result := mustTunnelDeleteFailureResult(t, out)
|
|
if contractValueAsInt64(result["failCount"]) != 1 {
|
|
t.Fatalf("expected failCount=1, got %#v", result["failCount"])
|
|
}
|
|
assertBatchFailureNameAndReason(t, result, "replace-forward", "节点 replace-target-node 端口 23001 已被其他转发占用")
|
|
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM tunnel WHERE id = ?`, sourceTunnelID); count != 1 {
|
|
t.Fatalf("expected source tunnel kept, got count=%d", count)
|
|
}
|
|
if tunnelAfter := mustQueryInt64(t, repo, `SELECT tunnel_id FROM forward WHERE id = ?`, forwardID); tunnelAfter != sourceTunnelID {
|
|
t.Fatalf("expected forward tunnel unchanged, got %d", tunnelAfter)
|
|
}
|
|
}
|
|
|
|
func TestTunnelBatchDeletePreviewIncludesTotalsContract(t *testing.T) {
|
|
secret := "contract-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
adminToken := mustAdminToken(t, secret)
|
|
now := time.Now().UnixMilli()
|
|
|
|
tunnelA, nodeA := seedTunnelDeleteTunnelWithNode(t, repo, now, "batch-preview-a", "batch-preview-node-a", "24000-24010")
|
|
tunnelB, _ := seedTunnelDeleteTunnelWithNode(t, repo, now, "batch-preview-b", "batch-preview-node-b", "24100-24110")
|
|
seedTunnelDeleteForward(t, repo, now, tunnelA, nodeA, "batch-preview-forward", 24001)
|
|
|
|
out := requestContractEnvelope(t, router, adminToken, "/api/v1/tunnel/batch-delete-preview", map[string]interface{}{
|
|
"ids": []int64{tunnelA, tunnelB},
|
|
})
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected success, got code=%d msg=%q", out.Code, out.Msg)
|
|
}
|
|
data, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected preview object, got %T", out.Data)
|
|
}
|
|
if contractValueAsInt64(data["tunnelCount"]) != 2 {
|
|
t.Fatalf("expected tunnelCount=2, got %#v", data["tunnelCount"])
|
|
}
|
|
if contractValueAsInt64(data["totalForwardCount"]) != 1 {
|
|
t.Fatalf("expected totalForwardCount=1, got %#v", data["totalForwardCount"])
|
|
}
|
|
}
|
|
|
|
func TestTunnelBatchDeleteWithForwardsReturnsTunnelLevelFailuresContract(t *testing.T) {
|
|
secret := "contract-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
adminToken := mustAdminToken(t, secret)
|
|
now := time.Now().UnixMilli()
|
|
|
|
sourceTunnelA, _ := seedTunnelDeleteTunnelWithNode(t, repo, now, "batch-replace-source-a", "batch-replace-source-node-a", "25000-25010")
|
|
sourceTunnelB, sourceNodeB := seedTunnelDeleteTunnelWithNode(t, repo, now, "batch-replace-source-b", "batch-replace-source-node-b", "25100-25110")
|
|
targetTunnelID, targetNodeID := seedTunnelDeleteTunnelWithNode(t, repo, now, "batch-replace-target", "batch-replace-target-node", "25000-25010")
|
|
|
|
seedTunnelDeleteForward(t, repo, now, sourceTunnelB, sourceNodeB, "batch-replace-forward-b", 25002)
|
|
seedTunnelDeleteForward(t, repo, now, targetTunnelID, targetNodeID, "batch-replace-occupied", 25002)
|
|
|
|
out := requestContractEnvelope(t, router, adminToken, "/api/v1/tunnel/batch-delete-with-forwards", map[string]interface{}{
|
|
"ids": []int64{sourceTunnelA, sourceTunnelB},
|
|
"action": "replace",
|
|
"targetTunnelId": targetTunnelID,
|
|
})
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected success envelope, got code=%d msg=%q", out.Code, out.Msg)
|
|
}
|
|
|
|
result := mustTunnelDeleteFailureResult(t, out)
|
|
if contractValueAsInt64(result["successCount"]) != 1 {
|
|
t.Fatalf("expected successCount=1, got %#v", result["successCount"])
|
|
}
|
|
if contractValueAsInt64(result["failCount"]) != 1 {
|
|
t.Fatalf("expected failCount=1, got %#v", result["failCount"])
|
|
}
|
|
assertBatchFailureNameAndReason(t, result, "batch-replace-source-b", "batch-replace-forward-b: 节点 batch-replace-target-node 端口 25002 已被其他转发占用")
|
|
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM tunnel WHERE id = ?`, sourceTunnelA); count != 0 {
|
|
t.Fatalf("expected source tunnel A deleted, got count=%d", count)
|
|
}
|
|
if count := mustQueryInt(t, repo, `SELECT COUNT(1) FROM tunnel WHERE id = ?`, sourceTunnelB); count != 1 {
|
|
t.Fatalf("expected source tunnel B kept, got count=%d", count)
|
|
}
|
|
}
|
|
|
|
func seedTunnelDeleteTunnelWithNode(t *testing.T, repo *storeRepo.Repository, now int64, tunnelName, nodeName, portRange string) (int64, int64) {
|
|
t.Helper()
|
|
|
|
if err := repo.DB().Exec(`
|
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, status, created_time, updated_time, in_ip, inx, ip_preference)
|
|
VALUES(?, 1.0, 1, 'tls', 1, 1, ?, ?, NULL, 0, '')
|
|
`, tunnelName, now, now).Error; err != nil {
|
|
t.Fatalf("insert tunnel %s: %v", tunnelName, err)
|
|
}
|
|
tunnelID := mustLastInsertID(t, repo, tunnelName)
|
|
|
|
if err := repo.DB().Exec(`
|
|
INSERT INTO node(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.0.0.1', '10.0.0.1', '', ?, '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0)
|
|
`, nodeName, nodeName+"-secret", portRange, now, now).Error; err != nil {
|
|
t.Fatalf("insert node %s: %v", nodeName, err)
|
|
}
|
|
nodeID := mustLastInsertID(t, repo, nodeName)
|
|
|
|
if err := repo.DB().Exec(`
|
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
|
VALUES(?, 1, ?, 0, 'round', 1, 'tls')
|
|
`, tunnelID, nodeID).Error; err != nil {
|
|
t.Fatalf("insert chain_tunnel for %s: %v", tunnelName, err)
|
|
}
|
|
|
|
return tunnelID, nodeID
|
|
}
|
|
|
|
func seedTunnelDeleteForward(t *testing.T, repo *storeRepo.Repository, now int64, tunnelID, nodeID int64, forwardName string, port int) int64 {
|
|
t.Helper()
|
|
|
|
if err := repo.DB().Exec(`
|
|
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
|
VALUES(2, 'contract-user', ?, ?, '1.1.1.1:443', 'fifo', 0, 0, ?, ?, 1, 0)
|
|
`, forwardName, tunnelID, now, now).Error; err != nil {
|
|
t.Fatalf("insert forward %s: %v", forwardName, err)
|
|
}
|
|
forwardID := mustLastInsertID(t, repo, forwardName)
|
|
|
|
if err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeID, port).Error; err != nil {
|
|
t.Fatalf("insert forward_port for %s: %v", forwardName, err)
|
|
}
|
|
|
|
return forwardID
|
|
}
|
|
|
|
func mustTunnelDeleteFailureResult(t *testing.T, out response.R) map[string]interface{} {
|
|
t.Helper()
|
|
result, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected result object, got %T", out.Data)
|
|
}
|
|
return result
|
|
}
|