mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-29 13:46:41 +00:00
[client] Extract pure evalConnStatus and add unit tests
Split isConnectedOnAllWay into a thin method that snapshots state and a pure evalConnStatus helper that takes a connStatusInputs struct, so the tri-state decision logic can be exercised without constructing full Worker or Handshaker objects. Add table-driven tests covering force-relay, ICE-unavailable and fully-available code paths, plus unit tests for iceRetryState budget/hourly transitions and reset.
This commit is contained in:
@@ -728,37 +728,22 @@ func (conn *Conn) isConnectedOnAllWay() (status guard.ConnStatus) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
relayConnected := conn.workerRelay.IsRelayConnectionSupportedWithPeer() &&
|
iceWorkerCreated := conn.workerICE != nil
|
||||||
conn.statusRelay.Get() == worker.StatusConnected
|
|
||||||
|
|
||||||
// Force-relay mode (JS/WASM or NB_FORCE_RELAY): ICE is never used, relay is the only transport.
|
var iceInProgress bool
|
||||||
if IsForceRelayed() {
|
if iceWorkerCreated {
|
||||||
return boolToConnStatus(relayConnected)
|
iceInProgress = conn.workerICE.InProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
iceAvailable := conn.handshaker.RemoteICESupported() && conn.workerICE != nil
|
return evalConnStatus(connStatusInputs{
|
||||||
|
forceRelay: IsForceRelayed(),
|
||||||
// When ICE is not available (remote peer doesn't support it or worker not yet created),
|
peerUsesRelay: conn.workerRelay.IsRelayConnectionSupportedWithPeer(),
|
||||||
// relay is the only possible transport.
|
relayConnected: conn.statusRelay.Get() == worker.StatusConnected,
|
||||||
if !iceAvailable {
|
remoteSupportsICE: conn.handshaker.RemoteICESupported(),
|
||||||
return boolToConnStatus(relayConnected)
|
iceWorkerCreated: iceWorkerCreated,
|
||||||
}
|
iceStatusConnecting: conn.statusICE.Get() != worker.StatusDisconnected,
|
||||||
|
iceInProgress: iceInProgress,
|
||||||
// ICE is considered "up" when it is connected or a connection attempt is in progress.
|
})
|
||||||
iceConnected := conn.statusICE.Get() != worker.StatusDisconnected || conn.workerICE.InProgress()
|
|
||||||
|
|
||||||
// Relay is OK if the peer doesn't use relay, or if relay is actually connected.
|
|
||||||
relayOK := !conn.workerRelay.IsRelayConnectionSupportedWithPeer() || relayConnected
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case iceConnected && relayOK:
|
|
||||||
return guard.ConnStatusConnected
|
|
||||||
case relayConnected:
|
|
||||||
// Relay is up but ICE is down — partially connected.
|
|
||||||
return guard.ConnStatusPartiallyConnected
|
|
||||||
default:
|
|
||||||
return guard.ConnStatusDisconnected
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) {
|
func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) {
|
||||||
@@ -947,6 +932,39 @@ func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
|||||||
return remoteRosenpassPubKey != nil
|
return remoteRosenpassPubKey != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func evalConnStatus(in connStatusInputs) guard.ConnStatus {
|
||||||
|
// "Relay up and needed" — the peer uses relay and the transport is connected.
|
||||||
|
relayUsedAndUp := in.peerUsesRelay && in.relayConnected
|
||||||
|
|
||||||
|
// Force-relay mode: ICE never runs. Relay is the only transport and must be up.
|
||||||
|
if in.forceRelay {
|
||||||
|
return boolToConnStatus(relayUsedAndUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote peer doesn't support ICE, or we haven't created the worker yet:
|
||||||
|
// relay is the only possible transport.
|
||||||
|
if !in.remoteSupportsICE || !in.iceWorkerCreated {
|
||||||
|
return boolToConnStatus(relayUsedAndUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICE counts as "up" when the status is anything other than Disconnected, OR
|
||||||
|
// when a negotiation is currently in progress (so we don't spam offers while one is in flight).
|
||||||
|
iceUp := in.iceStatusConnecting || in.iceInProgress
|
||||||
|
|
||||||
|
// Relay side is acceptable if the peer doesn't rely on relay, or relay is connected.
|
||||||
|
relayOK := !in.peerUsesRelay || in.relayConnected
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case iceUp && relayOK:
|
||||||
|
return guard.ConnStatusConnected
|
||||||
|
case relayUsedAndUp:
|
||||||
|
// Relay is up but ICE is down — partially connected.
|
||||||
|
return guard.ConnStatusPartiallyConnected
|
||||||
|
default:
|
||||||
|
return guard.ConnStatusDisconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func boolToConnStatus(connected bool) guard.ConnStatus {
|
func boolToConnStatus(connected bool) guard.ConnStatus {
|
||||||
if connected {
|
if connected {
|
||||||
return guard.ConnStatusConnected
|
return guard.ConnStatusConnected
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const (
|
|||||||
StatusConnected
|
StatusConnected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// connStatusInputs is the primitive-valued snapshot of the state that drives the
|
||||||
|
// tri-state connection classification. Extracted so the decision logic can be unit-tested
|
||||||
|
// without constructing full Worker/Handshaker objects.
|
||||||
|
type connStatusInputs struct {
|
||||||
|
forceRelay bool // NB_FORCE_RELAY or JS/WASM
|
||||||
|
peerUsesRelay bool // remote peer advertises relay support AND local has relay
|
||||||
|
relayConnected bool // statusRelay reports Connected (independent of whether peer uses relay)
|
||||||
|
remoteSupportsICE bool // remote peer sent ICE credentials
|
||||||
|
iceWorkerCreated bool // local WorkerICE exists (false in force-relay mode)
|
||||||
|
iceStatusConnecting bool // statusICE is anything other than Disconnected
|
||||||
|
iceInProgress bool // a negotiation is currently in flight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ConnStatus describe the status of a peer's connection
|
// ConnStatus describe the status of a peer's connection
|
||||||
type ConnStatus int32
|
type ConnStatus int32
|
||||||
|
|
||||||
|
|||||||
201
client/internal/peer/conn_status_eval_test.go
Normal file
201
client/internal/peer/conn_status_eval_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvalConnStatus_ForceRelay(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in connStatusInputs
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "force relay, peer uses relay, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force relay, peer uses relay, relay down",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: false,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force relay, peer does NOT use relay - disconnected forever",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: false,
|
||||||
|
relayConnected: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := evalConnStatus(tc.in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalConnStatus_ICEUnavailable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in connStatusInputs
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer uses relay, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer uses relay, relay down",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: false,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE worker not yet created, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
remoteSupportsICE: true,
|
||||||
|
iceWorkerCreated: false,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer does not use relay",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: false,
|
||||||
|
relayConnected: false,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := evalConnStatus(tc.in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalConnStatus_FullyAvailable(t *testing.T) {
|
||||||
|
base := connStatusInputs{
|
||||||
|
remoteSupportsICE: true,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mutator func(*connStatusInputs)
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ICE connected, relay connected, peer uses relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = true
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE connected, peer does NOT use relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE InProgress only, peer does NOT use relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, relay up, peer uses relay -> partial",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = true
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusPartiallyConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, peer does NOT use relay -> disconnected",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE up, peer uses relay but relay down -> partial (relay required, ICE ignored)",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
// relayOK = false (peer uses relay but it's down), iceUp = true
|
||||||
|
// first switch arm fails (relayOK false), relayUsedAndUp = false (relay down),
|
||||||
|
// falls into default: Disconnected.
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, relay up but peer does not use relay -> disconnected",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = true // not actually used since peer doesn't rely on it
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
in := base
|
||||||
|
tc.mutator(&in)
|
||||||
|
if got := evalConnStatus(in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v (inputs: %+v)", got, tc.want, in)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
103
client/internal/peer/guard/ice_retry_state_test.go
Normal file
103
client/internal/peer/guard/ice_retry_state_test.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package guard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestRetryState() *iceRetryState {
|
||||||
|
return &iceRetryState{log: log.NewEntry(log.StandardLogger())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_AllowsInitialBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
for i := 1; i <= maxICERetries; i++ {
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false on attempt %d, want true (budget = %d)", i, maxICERetries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ExhaustsAfterBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
for i := 0; i < maxICERetries; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned true after budget exhausted, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_HourlyCNilBeforeEnterHourlyMode(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC returned non-nil channel before enterHourlyMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_EnterHourlyModeArmsTicker(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
for i := 0; i < maxICERetries+1; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.enterHourlyMode()
|
||||||
|
defer s.reset()
|
||||||
|
|
||||||
|
if s.hourlyC() == nil {
|
||||||
|
t.Fatalf("hourlyC returned nil after enterHourlyMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ShouldRetryTrueInHourlyMode(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
s.enterHourlyMode()
|
||||||
|
defer s.reset()
|
||||||
|
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false in hourly mode, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent calls also return true — we keep retrying on each hourly tick.
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("second shouldRetry returned false in hourly mode, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ResetRestoresBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
for i := 0; i < maxICERetries+1; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
s.enterHourlyMode()
|
||||||
|
|
||||||
|
s.reset()
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC returned non-nil channel after reset")
|
||||||
|
}
|
||||||
|
if s.retries != 0 {
|
||||||
|
t.Fatalf("retries = %d after reset, want 0", s.retries)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= maxICERetries; i++ {
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false on attempt %d after reset, want true", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ResetIsIdempotent(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
s.reset()
|
||||||
|
s.reset() // second call must not panic or re-stop a nil ticker
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC non-nil after double reset")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user