mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 07:19:08 +01:00
197 lines
7.8 KiB
Go
197 lines
7.8 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
|
"github.com/netbirdio/netbird/management/server/account"
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
)
|
|
|
|
func TestCancelPeerRoutines_SessionOwnership(t *testing.T) {
|
|
peer := &nbpeer.Peer{ID: "peer-1", Key: "peer-key"}
|
|
streamStart := time.Unix(1700000000, 0)
|
|
session := make(chan *network_map.UpdateMessage)
|
|
|
|
tests := []struct {
|
|
name string
|
|
session chan *network_map.UpdateMessage
|
|
ownsPeer bool
|
|
cancelRefresh bool
|
|
}{
|
|
{name: "owning session tears everything down", session: session, ownsPeer: true, cancelRefresh: true},
|
|
{name: "stale session keeps the newer session's refresh", session: session, ownsPeer: false, cancelRefresh: false},
|
|
{name: "failed sync without a channel closes the older session", session: nil, ownsPeer: true, cancelRefresh: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
accountManager := account.NewMockManager(ctrl)
|
|
controller := network_map.NewMockController(ctrl)
|
|
secretsManager := NewMockSecretsManager(ctrl)
|
|
s := &Server{accountManager: accountManager, networkMapController: controller, secretsManager: secretsManager}
|
|
|
|
accountManager.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.Key, streamStart).Return(nil)
|
|
controller.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.ID, tt.session).Return(tt.ownsPeer)
|
|
if tt.cancelRefresh {
|
|
secretsManager.EXPECT().CancelRefresh(peer.ID)
|
|
// The stream's challenge renewal ends with it; a stale session leaves the
|
|
// newer one's alone.
|
|
accountManager.EXPECT().UntrackCertificateChallenges("account-1", peer.ID, streamStart)
|
|
}
|
|
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, streamStart, tt.session)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCancelPeerRoutines_DrainingQueuesDisconnect(t *testing.T) {
|
|
streamStart := time.Unix(1700000000, 0)
|
|
|
|
tests := []struct {
|
|
name string
|
|
embedded bool
|
|
draining bool
|
|
flushBefore bool
|
|
wantQueued bool
|
|
wantOwnWrite bool
|
|
}{
|
|
{name: "regular peer while draining is queued", draining: true, wantQueued: true},
|
|
{name: "embedded proxy peer while draining writes its own disconnect", embedded: true, draining: true, wantOwnWrite: true},
|
|
{name: "regular peer without draining writes its own disconnect", wantOwnWrite: true},
|
|
{name: "regular peer after the flush writes its own disconnect", draining: true, flushBefore: true, wantOwnWrite: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
accountManager := account.NewMockManager(ctrl)
|
|
controller := network_map.NewMockController(ctrl)
|
|
secretsManager := NewMockSecretsManager(ctrl)
|
|
s := &Server{accountManager: accountManager, networkMapController: controller, secretsManager: secretsManager}
|
|
|
|
peer := &nbpeer.Peer{ID: "peer-1", Key: "peer-key"}
|
|
peer.ProxyMeta.Embedded = tt.embedded
|
|
|
|
if tt.draining {
|
|
s.StartDraining()
|
|
}
|
|
if tt.flushBefore {
|
|
require.NoError(t, s.FlushDisconnects(context.Background()))
|
|
}
|
|
|
|
if tt.wantOwnWrite {
|
|
accountManager.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.Key, streamStart).Return(nil)
|
|
}
|
|
controller.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.ID, gomock.Any()).Return(true)
|
|
secretsManager.EXPECT().CancelRefresh(peer.ID)
|
|
accountManager.EXPECT().UntrackCertificateChallenges("account-1", peer.ID, streamStart)
|
|
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, streamStart, nil)
|
|
|
|
if tt.wantQueued {
|
|
accountManager.EXPECT().MarkPeersDisconnected(gomock.Any(), []types.PeerSession{
|
|
{AccountID: "account-1", PeerID: peer.ID, SessionStartedAt: streamStart.UnixNano()},
|
|
}).Return(nil)
|
|
}
|
|
require.NoError(t, s.FlushDisconnects(context.Background()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlushDisconnects_FailedBatchStopsDraining(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
accountManager := account.NewMockManager(ctrl)
|
|
controller := network_map.NewMockController(ctrl)
|
|
secretsManager := NewMockSecretsManager(ctrl)
|
|
s := &Server{accountManager: accountManager, networkMapController: controller, secretsManager: secretsManager}
|
|
|
|
peer := &nbpeer.Peer{ID: "peer-1", Key: "peer-key"}
|
|
first := time.Unix(1700000000, 0)
|
|
second := first.Add(time.Second)
|
|
controller.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.ID, gomock.Any()).Return(true).Times(2)
|
|
secretsManager.EXPECT().CancelRefresh(peer.ID).Times(2)
|
|
accountManager.EXPECT().UntrackCertificateChallenges("account-1", peer.ID, gomock.Any()).Times(2)
|
|
|
|
s.StartDraining()
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, first, nil)
|
|
|
|
accountManager.EXPECT().MarkPeersDisconnected(gomock.Any(), gomock.Any()).Return(errors.New("db down")).Times(2)
|
|
require.Error(t, s.FlushDisconnects(context.Background()))
|
|
|
|
accountManager.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.Key, second).Return(nil)
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, second, nil)
|
|
}
|
|
|
|
func TestFlushDisconnects_RetriesFailedBatch(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
accountManager := account.NewMockManager(ctrl)
|
|
controller := network_map.NewMockController(ctrl)
|
|
secretsManager := NewMockSecretsManager(ctrl)
|
|
s := &Server{accountManager: accountManager, networkMapController: controller, secretsManager: secretsManager}
|
|
|
|
peer := &nbpeer.Peer{ID: "peer-1", Key: "peer-key"}
|
|
streamStart := time.Unix(1700000000, 0)
|
|
controller.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.ID, gomock.Any()).Return(true)
|
|
secretsManager.EXPECT().CancelRefresh(peer.ID)
|
|
accountManager.EXPECT().UntrackCertificateChallenges("account-1", peer.ID, streamStart)
|
|
|
|
s.StartDraining()
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, streamStart, nil)
|
|
|
|
want := []types.PeerSession{{AccountID: "account-1", PeerID: peer.ID, SessionStartedAt: streamStart.UnixNano()}}
|
|
gomock.InOrder(
|
|
accountManager.EXPECT().MarkPeersDisconnected(gomock.Any(), want).Return(errors.New("deadlock detected")),
|
|
accountManager.EXPECT().MarkPeersDisconnected(gomock.Any(), want).Return(nil),
|
|
)
|
|
require.NoError(t, s.FlushDisconnects(context.Background()))
|
|
}
|
|
|
|
func TestCancelPeerRoutines_QueuesDisconnectBeforeWaitingForPeerLock(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
accountManager := account.NewMockManager(ctrl)
|
|
controller := network_map.NewMockController(ctrl)
|
|
secretsManager := NewMockSecretsManager(ctrl)
|
|
s := &Server{accountManager: accountManager, networkMapController: controller, secretsManager: secretsManager}
|
|
|
|
peer := &nbpeer.Peer{ID: "peer-1", Key: "peer-key"}
|
|
streamStart := time.Unix(1700000000, 0)
|
|
controller.EXPECT().OnPeerDisconnected(gomock.Any(), "account-1", peer.ID, gomock.Any()).Return(true)
|
|
secretsManager.EXPECT().CancelRefresh(peer.ID)
|
|
accountManager.EXPECT().UntrackCertificateChallenges("account-1", peer.ID, streamStart)
|
|
|
|
unlock := s.acquirePeerLockByUID(context.Background(), peer.Key)
|
|
var unlockOnce sync.Once
|
|
t.Cleanup(func() { unlockOnce.Do(unlock) })
|
|
s.StartDraining()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
s.cancelPeerRoutines(context.Background(), "account-1", peer, streamStart, nil)
|
|
}()
|
|
|
|
require.Eventually(t, func() bool {
|
|
s.drainMu.Lock()
|
|
defer s.drainMu.Unlock()
|
|
return len(s.pendingDisconnects) == 1
|
|
}, time.Second, 5*time.Millisecond, "the disconnect must be queued while the teardown waits for the peer lock")
|
|
|
|
accountManager.EXPECT().MarkPeersDisconnected(gomock.Any(), []types.PeerSession{
|
|
{AccountID: "account-1", PeerID: peer.ID, SessionStartedAt: streamStart.UnixNano()},
|
|
}).Return(nil)
|
|
require.NoError(t, s.FlushDisconnects(context.Background()))
|
|
|
|
unlockOnce.Do(unlock)
|
|
<-done
|
|
}
|