mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
452 lines
18 KiB
Go
452 lines
18 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var errBestExitProbeForTest = errors.New("probe failed")
|
|
|
|
func TestBestExitScoreCombinesLatencyAndLoss(t *testing.T) {
|
|
exit := chainNodeRecord{NodeID: 30, NodeName: "exit-a"}
|
|
score := scoreBestExitCandidate(10, exit, 25, 2, 80, 3)
|
|
|
|
if !score.Success {
|
|
t.Fatalf("expected successful score")
|
|
}
|
|
if score.OwnerNodeID != 10 || score.ExitNodeID != 30 {
|
|
t.Fatalf("unexpected owner/exit ids: %+v", score)
|
|
}
|
|
if score.TotalLatency != 105 {
|
|
t.Fatalf("expected total latency 105, got %v", score.TotalLatency)
|
|
}
|
|
if score.TotalLoss < 4.9 || score.TotalLoss > 5.0 {
|
|
t.Fatalf("expected combined loss about 4.94, got %v", score.TotalLoss)
|
|
}
|
|
if score.Score < 599 || score.Score > 600 {
|
|
t.Fatalf("expected score about 599, got %v", score.Score)
|
|
}
|
|
}
|
|
|
|
func TestBestExitScorePenalizesLoss(t *testing.T) {
|
|
stable := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 80, 0, 80, 0)
|
|
lowLatencyLossy := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 10, 5, 10, 5)
|
|
|
|
if !bestExitScoreLess(stable, lowLatencyLossy) {
|
|
t.Fatalf("expected stable exit to beat low-latency lossy exit: stable=%+v lossy=%+v", stable, lowLatencyLossy)
|
|
}
|
|
}
|
|
|
|
func TestBestExitFailedCandidateSortsLast(t *testing.T) {
|
|
failed := failedBestExitCandidate(10, chainNodeRecord{NodeID: 30}, "dial timeout")
|
|
good := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 100, 0, 100, 0)
|
|
|
|
scores := []bestExitCandidateScore{failed, good}
|
|
sortBestExitScores(scores)
|
|
|
|
if scores[0].ExitNodeID != 31 || scores[1].ExitNodeID != 30 {
|
|
t.Fatalf("expected good score first and failed score last, got %+v", scores)
|
|
}
|
|
}
|
|
|
|
func TestBestExitInitialObservationAppliesWithoutSwitch(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate}, now)
|
|
if decision.Switch {
|
|
t.Fatalf("initial observation should not return switch: %+v", decision)
|
|
}
|
|
if m.decisions[key].AppliedExitNodeID != 31 {
|
|
t.Fatalf("expected applied exit 31, got %+v", m.decisions[key])
|
|
}
|
|
}
|
|
|
|
func TestBestExitDecisionRequiresMinimumAdvantage(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 90, 0, 90, 0)
|
|
|
|
m.setApplied(key, 30, now.Add(-time.Minute))
|
|
for i := 0; i < bestExitConfirmationRounds+1; i++ {
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(time.Duration(i)*time.Second))
|
|
if decision.Switch {
|
|
t.Fatalf("candidate below minimum advantage should not switch after repeated observations: %+v", decision)
|
|
}
|
|
}
|
|
if m.decisions[key].AppliedExitNodeID != 30 {
|
|
t.Fatalf("expected applied exit to remain 30, got %+v", m.decisions[key])
|
|
}
|
|
}
|
|
|
|
func TestBestExitDecisionSwitchesWithMinimumAdvantage(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
|
|
m.setApplied(key, 30, now.Add(-time.Minute))
|
|
for i := 0; i < bestExitConfirmationRounds-1; i++ {
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(time.Duration(i)*time.Second))
|
|
if decision.Switch {
|
|
t.Fatalf("candidate should wait for confirmations before switching: %+v", decision)
|
|
}
|
|
}
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add((bestExitConfirmationRounds-1)*time.Second))
|
|
if !decision.Switch || decision.ExitNodeID != 31 {
|
|
t.Fatalf("candidate with enough advantage should switch after confirmations: %+v", decision)
|
|
}
|
|
}
|
|
|
|
func TestBestExitConfirmedSwitchDoesNotMarkAppliedUntilSetApplied(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
|
|
m.setApplied(key, 30, now.Add(-time.Minute))
|
|
for i := 0; i < bestExitConfirmationRounds-1; i++ {
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(time.Duration(i)*time.Second))
|
|
if decision.Switch {
|
|
t.Fatalf("candidate should wait for confirmations before switching: %+v", decision)
|
|
}
|
|
}
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add((bestExitConfirmationRounds-1)*time.Second))
|
|
if !decision.Switch || decision.ExitNodeID != 31 {
|
|
t.Fatalf("candidate with enough advantage should switch after confirmations: %+v", decision)
|
|
}
|
|
if m.decisions[key].AppliedExitNodeID != 30 {
|
|
t.Fatalf("confirmed switch should not mark applied before runtime update: %+v", m.decisions[key])
|
|
}
|
|
|
|
m.setApplied(key, decision.ExitNodeID, now.Add(time.Second))
|
|
if m.decisions[key].AppliedExitNodeID != 31 {
|
|
t.Fatalf("setApplied should commit confirmed switch: %+v", m.decisions[key])
|
|
}
|
|
}
|
|
|
|
func TestBestExitApplyFailureStartsRetryCooldownWithoutChangingAppliedExit(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
|
|
m.setApplied(key, 30, now.Add(-time.Minute))
|
|
for i := 0; i < bestExitConfirmationRounds-1; i++ {
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(time.Duration(i)*time.Second))
|
|
if decision.Switch {
|
|
t.Fatalf("candidate should wait for confirmations before switching: %+v", decision)
|
|
}
|
|
}
|
|
confirmed := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add((bestExitConfirmationRounds-1)*time.Second))
|
|
if !confirmed.Switch || confirmed.ExitNodeID != 31 {
|
|
t.Fatalf("expected confirmed switch before apply failure: %+v", confirmed)
|
|
}
|
|
|
|
m.recordApplyFailure(key, confirmed.ExitNodeID, now.Add(bestExitConfirmationRounds*time.Second))
|
|
if m.decisions[key].AppliedExitNodeID != 30 {
|
|
t.Fatalf("apply failure should leave applied exit unchanged: %+v", m.decisions[key])
|
|
}
|
|
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add((bestExitConfirmationRounds+1)*time.Second))
|
|
if decision.Switch {
|
|
t.Fatalf("apply retry cooldown should suppress immediate retry: %+v", decision)
|
|
}
|
|
if decision.Reason != "apply retry cooldown" {
|
|
t.Fatalf("expected apply retry cooldown reason, got %q", decision.Reason)
|
|
}
|
|
|
|
retry := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(bestExitConfirmationRounds*time.Second+bestExitApplyRetryCooldown))
|
|
if !retry.Switch || retry.ExitNodeID != 31 {
|
|
t.Fatalf("expected retry after apply cooldown: %+v", retry)
|
|
}
|
|
}
|
|
|
|
func TestBestExitEnsureAppliedDoesNotOverrideExistingAppliedExit(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
|
|
m.ensureApplied(key, 30, now)
|
|
if m.decisions[key].AppliedExitNodeID != 30 {
|
|
t.Fatalf("expected initial applied exit 30, got %+v", m.decisions[key])
|
|
}
|
|
if !m.decisions[key].LastSwitchAt.Equal(now) {
|
|
t.Fatalf("expected initial applied timestamp, got %+v", m.decisions[key])
|
|
}
|
|
|
|
m.ensureApplied(key, 31, now.Add(time.Minute))
|
|
if m.decisions[key].AppliedExitNodeID != 30 {
|
|
t.Fatalf("ensureApplied should not override existing applied exit: %+v", m.decisions[key])
|
|
}
|
|
}
|
|
|
|
func TestBestExitRoundPingerCachesByNodeHostAndPort(t *testing.T) {
|
|
publicCalls := 0
|
|
ownerCalls := 0
|
|
pinger := newBestExitRoundPinger(func(nodeID int64, ip string, port int, _ diagnosisExecOptions) (float64, float64, error) {
|
|
if ip == bestExitPublicTargetHost && port == bestExitPublicTargetPort {
|
|
publicCalls++
|
|
return float64(nodeID), 0, nil
|
|
}
|
|
ownerCalls++
|
|
return float64(ownerCalls), 0, nil
|
|
})
|
|
|
|
if lat, _, err := pinger(30, bestExitPublicTargetHost, bestExitPublicTargetPort, diagnosisExecOptions{}); err != nil || lat != 30 {
|
|
t.Fatalf("unexpected first public ping result lat=%v err=%v", lat, err)
|
|
}
|
|
if lat, _, err := pinger(30, bestExitPublicTargetHost, bestExitPublicTargetPort, diagnosisExecOptions{}); err != nil || lat != 30 {
|
|
t.Fatalf("unexpected cached public ping result lat=%v err=%v", lat, err)
|
|
}
|
|
if _, _, err := pinger(31, bestExitPublicTargetHost, bestExitPublicTargetPort, diagnosisExecOptions{}); err != nil {
|
|
t.Fatalf("unexpected second exit public ping err=%v", err)
|
|
}
|
|
if publicCalls != 2 {
|
|
t.Fatalf("expected public probes cached per exit node, got %d calls", publicCalls)
|
|
}
|
|
|
|
if _, _, err := pinger(10, "10.0.0.30", 30030, diagnosisExecOptions{}); err != nil {
|
|
t.Fatalf("unexpected owner ping err=%v", err)
|
|
}
|
|
if _, _, err := pinger(10, "10.0.0.30", 30030, diagnosisExecOptions{}); err != nil {
|
|
t.Fatalf("unexpected repeated owner ping err=%v", err)
|
|
}
|
|
if ownerCalls != 1 {
|
|
t.Fatalf("expected owner-to-exit probes cached by target, got %d calls", ownerCalls)
|
|
}
|
|
}
|
|
|
|
func TestBestExitDecisionScoresAreDefensiveCopies(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now)
|
|
decision.Scores[0].ExitNodeID = 99
|
|
|
|
if m.decisions[key].Scores[0].ExitNodeID != 31 {
|
|
t.Fatalf("decision scores mutation leaked into manager state: %+v", m.decisions[key].Scores)
|
|
}
|
|
}
|
|
|
|
func TestBestExitDecisionRequiresConfirmationsAndCooldown(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
now := time.Unix(100, 0)
|
|
current := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 100, 0, 100, 0)
|
|
candidate := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 31}, 40, 0, 60, 0)
|
|
|
|
m.setApplied(key, 30, now.Add(-time.Minute))
|
|
|
|
if decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now); decision.Switch {
|
|
t.Fatalf("first observation should not switch: %+v", decision)
|
|
}
|
|
if decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(time.Second)); decision.Switch {
|
|
t.Fatalf("second observation should not switch: %+v", decision)
|
|
}
|
|
decision := m.observeScores(key, []bestExitCandidateScore{candidate, current}, now.Add(2*time.Second))
|
|
if !decision.Switch || decision.ExitNodeID != 31 {
|
|
t.Fatalf("third confirmed observation should switch to 31: %+v", decision)
|
|
}
|
|
|
|
betterAgain := scoreBestExitCandidate(10, chainNodeRecord{NodeID: 30}, 20, 0, 20, 0)
|
|
if decision := m.observeScores(key, []bestExitCandidateScore{betterAgain, candidate}, now.Add(3*time.Second)); decision.Switch {
|
|
t.Fatalf("cooldown should block immediate switch back: %+v", decision)
|
|
}
|
|
}
|
|
|
|
func TestBestExitOrderingUsesAppliedDecision(t *testing.T) {
|
|
m := newBestExitManager()
|
|
key := bestExitOwnerKey{TunnelID: 7, OwnerNodeID: 10}
|
|
m.setApplied(key, 31, time.Unix(100, 0))
|
|
targets := []tunnelRuntimeNode{
|
|
{NodeID: 30, Strategy: tunnelStrategyBest},
|
|
{NodeID: 31, Strategy: tunnelStrategyBest},
|
|
{NodeID: 32, Strategy: tunnelStrategyBest},
|
|
}
|
|
|
|
ordered := m.orderTargets(key, targets)
|
|
if ordered[0].NodeID != 31 || ordered[1].NodeID != 30 || ordered[2].NodeID != 32 {
|
|
t.Fatalf("unexpected order: %+v", ordered)
|
|
}
|
|
if targets[0].NodeID != 30 {
|
|
t.Fatalf("orderTargets mutated input: %+v", targets)
|
|
}
|
|
}
|
|
|
|
func TestBuildTunnelChainConfigMapsBestStrategyToFIFO(t *testing.T) {
|
|
nodes := map[int64]*nodeRecord{
|
|
10: {ID: 10, ServerIP: "10.0.0.10", ServerIPv4: "10.0.0.10", TCPListenAddr: "[::]"},
|
|
30: {ID: 30, ServerIP: "10.0.0.30", ServerIPv4: "10.0.0.30", TCPListenAddr: "[::]"},
|
|
31: {ID: 31, ServerIP: "10.0.0.31", ServerIPv4: "10.0.0.31", TCPListenAddr: "[::]"},
|
|
}
|
|
targets := []tunnelRuntimeNode{
|
|
{NodeID: 30, Port: 30030, Protocol: "tls", Strategy: tunnelStrategyBest, ChainType: 3},
|
|
{NodeID: 31, Port: 30031, Protocol: "tls", Strategy: tunnelStrategyBest, ChainType: 3},
|
|
}
|
|
|
|
chainData, err := buildTunnelChainConfig(77, 10, targets, nodes, "")
|
|
if err != nil {
|
|
t.Fatalf("build chain: %v", err)
|
|
}
|
|
hops := chainData["hops"].([]map[string]interface{})
|
|
selector := hops[0]["selector"].(map[string]interface{})
|
|
if selector["strategy"] != bestExitRuntimeStrategy {
|
|
t.Fatalf("expected best to render as fifo, got %v", selector["strategy"])
|
|
}
|
|
}
|
|
|
|
func TestHandlerOrdersBestExitTargetsForOwner(t *testing.T) {
|
|
h := &Handler{bestExit: newBestExitManager()}
|
|
key := bestExitOwnerKey{TunnelID: 77, OwnerNodeID: 10}
|
|
h.bestExit.setApplied(key, 31, time.Unix(100, 0))
|
|
targets := []tunnelRuntimeNode{
|
|
{NodeID: 30, Port: 30030, Strategy: tunnelStrategyBest},
|
|
{NodeID: 31, Port: 30031, Strategy: tunnelStrategyBest},
|
|
}
|
|
|
|
ordered := h.orderBestExitTargets(77, 10, targets)
|
|
if ordered[0].NodeID != 31 || ordered[1].NodeID != 30 {
|
|
t.Fatalf("unexpected ordered targets: %+v", ordered)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeStrategyForTargetsMapsBestTargetStrategyToFIFO(t *testing.T) {
|
|
owner := tunnelRuntimeNode{Strategy: "round"}
|
|
targets := []tunnelRuntimeNode{{Strategy: tunnelStrategyBest}}
|
|
|
|
if got := runtimeStrategyForTargets(owner, targets); got != bestExitRuntimeStrategy {
|
|
t.Fatalf("expected best target strategy to map to fifo, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeStrategyForTargetsPreservesNonBestTargetStrategy(t *testing.T) {
|
|
owner := tunnelRuntimeNode{Strategy: tunnelStrategyBest}
|
|
targets := []tunnelRuntimeNode{{Strategy: "round"}}
|
|
|
|
if got := runtimeStrategyForTargets(owner, targets); got != "round" {
|
|
t.Fatalf("expected target strategy round to remain unchanged, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeStrategyForTargetsMapsBestOwnerStrategyWhenTargetsEmpty(t *testing.T) {
|
|
owner := tunnelRuntimeNode{Strategy: tunnelStrategyBest}
|
|
|
|
if got := runtimeStrategyForTargets(owner, nil); got != bestExitRuntimeStrategy {
|
|
t.Fatalf("expected best owner fallback strategy to map to fifo, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateBestExitOwnerScoresAllCandidates(t *testing.T) {
|
|
owner := chainNodeRecord{NodeID: 10, NodeName: "entry"}
|
|
exits := []chainNodeRecord{
|
|
{NodeID: 30, NodeName: "exit-a", Port: 30030},
|
|
{NodeID: 31, NodeName: "exit-b", Port: 30031},
|
|
}
|
|
nodes := map[int64]*nodeRecord{
|
|
10: {ID: 10, ServerIP: "10.0.0.10", ServerIPv4: "10.0.0.10", TCPListenAddr: "[::]"},
|
|
30: {ID: 30, ServerIP: "10.0.0.30", ServerIPv4: "10.0.0.30", TCPListenAddr: "[::]"},
|
|
31: {ID: 31, ServerIP: "10.0.0.31", ServerIPv4: "10.0.0.31", TCPListenAddr: "[::]"},
|
|
}
|
|
pinger := func(nodeID int64, ip string, port int, _ diagnosisExecOptions) (float64, float64, error) {
|
|
switch {
|
|
case nodeID == 10 && port == 30030:
|
|
return 60, 0, nil
|
|
case nodeID == 10 && port == 30031:
|
|
return 20, 0, nil
|
|
case nodeID == 30 && ip == bestExitPublicTargetHost:
|
|
return 60, 0, nil
|
|
case nodeID == 31 && ip == bestExitPublicTargetHost:
|
|
return 20, 0, nil
|
|
default:
|
|
t.Fatalf("unexpected ping node=%d ip=%s port=%d", nodeID, ip, port)
|
|
return 0, 100, nil
|
|
}
|
|
}
|
|
|
|
scores := evaluateBestExitOwner(owner, exits, nodes, "", diagnosisExecOptions{}, defaultTunnelProbeTarget(), pinger)
|
|
if len(scores) != 2 {
|
|
t.Fatalf("expected two scores, got %+v", scores)
|
|
}
|
|
if scores[0].ExitNodeID != 31 {
|
|
t.Fatalf("expected exit-b first, got %+v", scores)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateBestExitOwnerUsesConfiguredPublicProbeTarget(t *testing.T) {
|
|
owner := chainNodeRecord{NodeID: 10, NodeName: "entry-a"}
|
|
exits := []chainNodeRecord{{NodeID: 30, NodeName: "exit-a", Port: 30001}}
|
|
nodes := map[int64]*nodeRecord{
|
|
10: {ID: 10, Name: "entry-a", ServerIP: "10.0.0.10", ServerIPv4: "10.0.0.10"},
|
|
30: {ID: 30, Name: "exit-a", ServerIP: "10.0.0.30", ServerIPv4: "10.0.0.30"},
|
|
}
|
|
target := tunnelProbeTarget{Host: "speed.example.com", Port: 8443}
|
|
var calls []string
|
|
ping := func(nodeID int64, ip string, port int, options diagnosisExecOptions) (float64, float64, error) {
|
|
calls = append(calls, fmt.Sprintf("%d|%s|%d", nodeID, ip, port))
|
|
return 10, 0, nil
|
|
}
|
|
|
|
scores := evaluateBestExitOwner(owner, exits, nodes, "", diagnosisExecOptions{}, target, ping)
|
|
if len(scores) != 1 || !scores[0].Success {
|
|
t.Fatalf("expected successful score, got %+v", scores)
|
|
}
|
|
if !slices.Contains(calls, "30|speed.example.com|8443") {
|
|
t.Fatalf("expected exit public probe to use configured target, calls=%+v", calls)
|
|
}
|
|
for _, call := range calls {
|
|
if strings.Contains(call, defaultTunnelProbeTargetHost) {
|
|
t.Fatalf("did not expect default target call when custom target configured: %+v", calls)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvaluateBestExitOwnerMarksCandidateFailedWhenOwnerToExitFails(t *testing.T) {
|
|
owner := chainNodeRecord{NodeID: 10, NodeName: "entry"}
|
|
exits := []chainNodeRecord{{NodeID: 30, NodeName: "exit-a", Port: 30030}}
|
|
nodes := map[int64]*nodeRecord{
|
|
10: {ID: 10, ServerIP: "10.0.0.10", ServerIPv4: "10.0.0.10", TCPListenAddr: "[::]"},
|
|
30: {ID: 30, ServerIP: "10.0.0.30", ServerIPv4: "10.0.0.30", TCPListenAddr: "[::]"},
|
|
}
|
|
pinger := func(nodeID int64, ip string, port int, _ diagnosisExecOptions) (float64, float64, error) {
|
|
return 0, 100, errBestExitProbeForTest
|
|
}
|
|
|
|
scores := evaluateBestExitOwner(owner, exits, nodes, "", diagnosisExecOptions{}, defaultTunnelProbeTarget(), pinger)
|
|
if len(scores) != 1 || scores[0].Success {
|
|
t.Fatalf("expected failed candidate, got %+v", scores)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateBestExitOwnerMarksCandidateFailedWhenTargetResolutionFails(t *testing.T) {
|
|
owner := chainNodeRecord{NodeID: 10, NodeName: "entry"}
|
|
exits := []chainNodeRecord{{NodeID: 30, NodeName: "exit-v6", Port: 30030}}
|
|
nodes := map[int64]*nodeRecord{
|
|
10: {ID: 10, Name: "entry", ServerIP: "10.0.0.10", ServerIPv4: "10.0.0.10", TCPListenAddr: "[::]"},
|
|
30: {ID: 30, Name: "exit-v6", ServerIP: "2001:db8::30", ServerIPv6: "2001:db8::30", TCPListenAddr: "[::]"},
|
|
}
|
|
pinger := func(nodeID int64, ip string, port int, _ diagnosisExecOptions) (float64, float64, error) {
|
|
t.Fatalf("ping should not be called when target resolution fails: node=%d ip=%s port=%d", nodeID, ip, port)
|
|
return 0, 100, nil
|
|
}
|
|
|
|
scores := evaluateBestExitOwner(owner, exits, nodes, "v4", diagnosisExecOptions{}, defaultTunnelProbeTarget(), pinger)
|
|
if len(scores) != 1 || scores[0].Success {
|
|
t.Fatalf("expected failed candidate, got %+v", scores)
|
|
}
|
|
}
|