From 6b4d4076f4b834590684dc8a88850861b47ef2eb Mon Sep 17 00:00:00 2001 From: pascal Date: Mon, 4 May 2026 15:16:59 +0200 Subject: [PATCH] extend tests --- management/server/affected_peers_test.go | 1214 +++++++++++++++++++--- 1 file changed, 1072 insertions(+), 142 deletions(-) diff --git a/management/server/affected_peers_test.go b/management/server/affected_peers_test.go index bf48b0e4a..0004fe5c1 100644 --- a/management/server/affected_peers_test.go +++ b/management/server/affected_peers_test.go @@ -15,6 +15,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" @@ -144,6 +145,30 @@ func TestCollectGroupChange_PolicyWithDirectPeerResource(t *testing.T) { assert.Contains(t, directPeers, peerIDs[3]) } +func TestCollectGroupChange_PolicyWithNonPeerResource_NoDirectPeers(t *testing.T) { + manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + _, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{ + Enabled: true, + Rules: []*types.PolicyRule{ + { + Enabled: true, + Sources: []string{groupIDs[0]}, + SourceResource: types.Resource{ID: "some-domain", Type: types.ResourceTypeDomain}, + Destinations: []string{groupIDs[1]}, + Action: types.PolicyTrafficActionAccept, + }, + }, + }, true) + require.NoError(t, err) + + groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]}) + assert.Contains(t, groups, groupIDs[0]) + assert.Contains(t, groups, groupIDs[1]) + assert.Empty(t, directPeers, "non-peer resources should not produce direct peer IDs") +} + func TestCollectGroupChange_RouteLinked(t *testing.T) { manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() @@ -277,6 +302,35 @@ func TestCollectGroupChange_NetworkRouterLinked(t *testing.T) { assert.Empty(t, directPeers) } +func TestCollectGroupChange_NetworkRouterPeerOnlyNoGroups(t *testing.T) { + manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + net1 := &networkTypes.Network{ + ID: "net-peer-only", + AccountID: accountID, + Name: "peer-only-network", + } + err := manager.Store.SaveNetwork(ctx, net1) + require.NoError(t, err) + + // Router with only a direct peer, no PeerGroups + err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{ + ID: "router-peer-only", + NetworkID: net1.ID, + AccountID: accountID, + Peer: peerIDs[4], + }) + require.NoError(t, err) + + // None of the groups should match since router has no PeerGroups + for i := 0; i < 5; i++ { + groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[i]}) + assert.Empty(t, groups, "group%d should not match router with only direct peer", i) + assert.Empty(t, directPeers, "group%d should not produce direct peers", i) + } +} + func TestCollectGroupChange_MultipleEntities(t *testing.T) { manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() @@ -329,6 +383,51 @@ func TestCollectGroupChange_MultipleEntities(t *testing.T) { assert.Empty(t, directPeers) } +func TestCollectGroupChange_MultipleNameServerGroups_OnlyLinkedAffected(t *testing.T) { + manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + // Create two nameserver groups using different groups + _, err := manager.CreateNameServerGroup(ctx, accountID, "ns-a", "NS-A", + []nbdns.NameServer{{ + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }}, + []string{groupIDs[0]}, + true, nil, true, userID, false, + ) + require.NoError(t, err) + + _, err = manager.CreateNameServerGroup(ctx, accountID, "ns-b", "NS-B", + []nbdns.NameServer{{ + IP: netip.MustParseAddr("8.8.8.8"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }}, + []string{groupIDs[2]}, + true, nil, true, userID, false, + ) + require.NoError(t, err) + + // Changing group0 should only find group0 (from ns-a), not group2 (from ns-b) + groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]}) + assert.Contains(t, groups, groupIDs[0]) + assert.NotContains(t, groups, groupIDs[2]) + + groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]}) + assert.Contains(t, groups, groupIDs[2]) + assert.NotContains(t, groups, groupIDs[0]) + + // Unrelated group + groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[4]}) + assert.Empty(t, groups) +} + +// --------------------------------------------------------------------------- +// collectPolicyAffectedGroupsAndPeers unit tests +// --------------------------------------------------------------------------- + func TestCollectPolicyAffectedGroups_Basic(t *testing.T) { policy := &types.Policy{ Rules: []*types.PolicyRule{ @@ -376,6 +475,47 @@ func TestCollectPolicyAffectedGroups_MultipleRules(t *testing.T) { assert.ElementsMatch(t, []string{"g1", "g2", "g3", "g4"}, groups) } +func TestCollectPolicyAffectedGroups_MultiplePolicies(t *testing.T) { + old := &types.Policy{ + Rules: []*types.PolicyRule{ + {Sources: []string{"g1"}, Destinations: []string{"g2"}}, + }, + } + new := &types.Policy{ + Rules: []*types.PolicyRule{ + {Sources: []string{"g3"}, Destinations: []string{"g4"}}, + }, + } + groups, _ := collectPolicyAffectedGroupsAndPeers(new, old) + assert.ElementsMatch(t, []string{"g1", "g2", "g3", "g4"}, groups) +} + +func TestCollectPolicyAffectedGroups_EmptyRules(t *testing.T) { + policy := &types.Policy{Rules: []*types.PolicyRule{}} + groups, directPeers := collectPolicyAffectedGroupsAndPeers(policy) + assert.Empty(t, groups) + assert.Empty(t, directPeers) +} + +func TestCollectPolicyAffectedGroups_NonPeerResource(t *testing.T) { + policy := &types.Policy{ + Rules: []*types.PolicyRule{ + { + Sources: []string{"g1"}, + SourceResource: types.Resource{ID: "domain-1", Type: types.ResourceTypeDomain}, + Destinations: []string{"g2"}, + }, + }, + } + groups, directPeers := collectPolicyAffectedGroupsAndPeers(policy) + assert.ElementsMatch(t, []string{"g1", "g2"}, groups) + assert.Empty(t, directPeers, "domain resource type should not produce direct peer IDs") +} + +// --------------------------------------------------------------------------- +// collectRouteAffectedGroupsAndPeers unit tests +// --------------------------------------------------------------------------- + func TestCollectRouteAffectedGroups_Basic(t *testing.T) { r := &route.Route{ Groups: []string{"g1"}, @@ -403,6 +543,105 @@ func TestCollectRouteAffectedGroups_NilRoute(t *testing.T) { assert.Nil(t, directPeers) } +func TestCollectRouteAffectedGroups_MultipleRoutes(t *testing.T) { + old := &route.Route{ + Groups: []string{"g1"}, + Peer: "p1", + } + new := &route.Route{ + Groups: []string{"g2"}, + PeerGroups: []string{"g3"}, + } + groups, directPeers := collectRouteAffectedGroupsAndPeers(new, old) + assert.ElementsMatch(t, []string{"g1", "g2", "g3"}, groups) + assert.ElementsMatch(t, []string{"p1"}, directPeers) +} + +// --------------------------------------------------------------------------- +// policyReferencesGroups / routeReferencesGroups / routerReferencesGroups +// --------------------------------------------------------------------------- + +func TestPolicyReferencesGroups(t *testing.T) { + policy := &types.Policy{ + Rules: []*types.PolicyRule{ + { + Sources: []string{"g1", "g2"}, + Destinations: []string{"g3"}, + }, + }, + } + + tests := []struct { + name string + groupSet map[string]struct{} + want bool + }{ + {"matches source", map[string]struct{}{"g1": {}}, true}, + {"matches destination", map[string]struct{}{"g3": {}}, true}, + {"no match", map[string]struct{}{"g4": {}}, false}, + {"empty set", map[string]struct{}{}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := policyReferencesGroups(policy, tt.groupSet) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRouteReferencesGroups(t *testing.T) { + r := &route.Route{ + Groups: []string{"g1"}, + PeerGroups: []string{"g2"}, + AccessControlGroups: []string{"g3"}, + } + + tests := []struct { + name string + groupSet map[string]struct{} + want bool + }{ + {"matches groups", map[string]struct{}{"g1": {}}, true}, + {"matches peerGroups", map[string]struct{}{"g2": {}}, true}, + {"matches accessControl", map[string]struct{}{"g3": {}}, true}, + {"no match", map[string]struct{}{"g4": {}}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := routeReferencesGroups(r, tt.groupSet) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRouterReferencesGroups(t *testing.T) { + router := &routerTypes.NetworkRouter{ + PeerGroups: []string{"g1", "g2"}, + } + + tests := []struct { + name string + groupSet map[string]struct{} + want bool + }{ + {"matches", map[string]struct{}{"g1": {}}, true}, + {"no match", map[string]struct{}{"g3": {}}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := routerReferencesGroups(router, tt.groupSet) + assert.Equal(t, tt.want, got) + }) + } +} + +// --------------------------------------------------------------------------- +// resolvePeerIDs tests +// --------------------------------------------------------------------------- + func TestResolvePeerIDs_GroupsOnly(t *testing.T) { manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() @@ -436,6 +675,10 @@ func TestResolvePeerIDs_EmptyInputs(t *testing.T) { assert.Empty(t, result) } +// --------------------------------------------------------------------------- +// resolveAffectedPeersForPeerChanges tests +// --------------------------------------------------------------------------- + func TestResolveAffectedPeers_NoPoliciesOrRoutes(t *testing.T) { manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t) ctx := context.Background() @@ -553,6 +796,38 @@ func TestResolveAffectedPeers_RouteWithDirectPeer(t *testing.T) { assert.ElementsMatch(t, []string{peerIDs[1], peerIDs[4]}, result) } +func TestResolveAffectedPeers_RouteWithAccessControlGroups(t *testing.T) { + manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + _, err := manager.CreateRoute(ctx, accountID, + netip.MustParsePrefix("10.7.0.0/24"), + route.IPv4Network, + nil, + "", + []string{groupIDs[0]}, + "acl route", + "aclnet", + false, + 9999, + []string{groupIDs[1]}, + []string{groupIDs[2]}, + true, + userID, + false, + false, + ) + require.NoError(t, err) + + // peer2 is only in AccessControlGroups, still should be affected + result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]}) + assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result) + + // peer3 is unrelated + result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[3]}) + assert.Empty(t, result) +} + func TestResolveAffectedPeers_NetworkRouter(t *testing.T) { manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() @@ -737,90 +1012,42 @@ func TestResolveAffectedPeers_EmptyChangedPeers(t *testing.T) { assert.Empty(t, result) } -func TestAffectedPeers_GroupUpdateOnlyAffectsLinkedPeers(t *testing.T) { - manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) +func TestResolveAffectedPeers_NoDuplicates(t *testing.T) { + manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() - accountID := account.Id - policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + err := manager.GroupAddPeer(ctx, accountID, groupIDs[1], peerIDs[0]) + require.NoError(t, err) + err = manager.GroupAddPeer(ctx, accountID, groupIDs[2], peerIDs[0]) require.NoError(t, err) - for _, p := range policies { - err := manager.Store.DeletePolicy(ctx, accountID, p.ID) - require.NoError(t, err) - } - - for _, g := range []*types.Group{ - {ID: "ap-grpA", Name: "AP-A", Peers: []string{peer1.ID}}, - {ID: "ap-grpB", Name: "AP-B", Peers: []string{peer2.ID}}, - {ID: "ap-grpC", Name: "AP-C", Peers: []string{peer3.ID}}, - } { - err := manager.CreateGroup(ctx, accountID, userID, g) - require.NoError(t, err) - } _, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{ Enabled: true, Rules: []*types.PolicyRule{ { - Enabled: true, - Sources: []string{"ap-grpA"}, - Destinations: []string{"ap-grpB"}, - Bidirectional: true, - Action: types.PolicyTrafficActionAccept, + Enabled: true, + Sources: []string{groupIDs[0], groupIDs[1]}, + Destinations: []string{groupIDs[2]}, + Action: types.PolicyTrafficActionAccept, }, }, }, true) require.NoError(t, err) - updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) - updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) - updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) - t.Cleanup(func() { - updateManager.CloseChannel(ctx, peer1.ID) - updateManager.CloseChannel(ctx, peer2.ID) - updateManager.CloseChannel(ctx, peer3.ID) - }) - - result := manager.resolveAffectedPeersForPeerChanges(ctx, manager.Store, accountID, []string{peer1.ID}) - assert.ElementsMatch(t, []string{peer1.ID, peer2.ID}, result) - - // Adding peer3 to grpA makes it part of the policy, so all 3 peers get updated - t.Run("group change updates all peers in policy groups", func(t *testing.T) { - done := make(chan struct{}) - go func() { - peerShouldReceiveUpdate(t, updMsg1) - peerShouldReceiveUpdate(t, updMsg2) - peerShouldReceiveUpdate(t, updMsg3) - close(done) - }() - - err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{ - ID: "ap-grpA", - Name: "AP-A", - Peers: []string{peer1.ID, peer3.ID}, - }) - assert.NoError(t, err) - - select { - case <-done: - case <-time.After(peerUpdateTimeout): - t.Error("timeout") - } - }) - - _ = updMsg1 - _ = updMsg2 - _ = updMsg3 -} - -func TestAffectedPeers_UnlinkedGroupChange_NoUpdates(t *testing.T) { - manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t) - ctx := context.Background() - result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]}) - assert.Empty(t, result) + count := 0 + for _, id := range result { + if id == peerIDs[0] { + count++ + } + } + assert.Equal(t, 1, count, "peer0 should appear exactly once") } +// --------------------------------------------------------------------------- +// Posture check affected peers tests +// --------------------------------------------------------------------------- + func TestCollectPostureCheckAffected_NoMatch(t *testing.T) { _, s, accountID, _, _ := setupAffectedPeersTest(t) ctx := context.Background() @@ -830,6 +1057,48 @@ func TestCollectPostureCheckAffected_NoMatch(t *testing.T) { assert.Empty(t, directPeers) } +func TestCollectPostureCheckAffected_LinkedToPolicy(t *testing.T) { + manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + // Create the posture check in the store so the policy validation keeps the reference. + err := s.SavePostureChecks(ctx, &posture.Checks{ + ID: "pc-1", + Name: "test-posture-check", + AccountID: accountID, + }) + require.NoError(t, err) + + policy, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{ + Enabled: true, + SourcePostureChecks: []string{"pc-1"}, + Rules: []*types.PolicyRule{ + { + Enabled: true, + Sources: []string{groupIDs[0]}, + Destinations: []string{groupIDs[1]}, + Action: types.PolicyTrafficActionAccept, + }, + }, + }, true) + require.NoError(t, err) + _ = policy + + groups, directPeers := collectPostureCheckAffectedGroupsAndPeers(ctx, s, accountID, "pc-1") + assert.Contains(t, groups, groupIDs[0]) + assert.Contains(t, groups, groupIDs[1]) + assert.Empty(t, directPeers) + + // Different posture check ID should not match + groups, directPeers = collectPostureCheckAffectedGroupsAndPeers(ctx, s, accountID, "pc-other") + assert.Empty(t, groups) + assert.Empty(t, directPeers) +} + +// --------------------------------------------------------------------------- +// Isolation tests: verify peers NOT in any relevant entity are NOT affected +// --------------------------------------------------------------------------- + func TestAffectedPeers_IsolatedPolicies(t *testing.T) { manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) ctx := context.Background() @@ -924,116 +1193,777 @@ func TestAffectedPeers_IsolatedRouteAndPolicy(t *testing.T) { assert.NotContains(t, result, peerIDs[1]) } -func TestResolveAffectedPeers_NoDuplicates(t *testing.T) { - manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) - ctx := context.Background() +// --------------------------------------------------------------------------- +// Integration tests with update channels (peerShouldReceiveUpdate / peerShouldNotReceiveUpdate) +// --------------------------------------------------------------------------- - err := manager.GroupAddPeer(ctx, accountID, groupIDs[1], peerIDs[0]) - require.NoError(t, err) - err = manager.GroupAddPeer(ctx, accountID, groupIDs[2], peerIDs[0]) +func TestAffectedPeers_GroupUpdateOnlyAffectsLinkedPeers(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "ap-grpA", Name: "AP-A", Peers: []string{peer1.ID}}, + {ID: "ap-grpB", Name: "AP-B", Peers: []string{peer2.ID}}, + {ID: "ap-grpC", Name: "AP-C", Peers: []string{peer3.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } _, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{ Enabled: true, Rules: []*types.PolicyRule{ { - Enabled: true, - Sources: []string{groupIDs[0], groupIDs[1]}, - Destinations: []string{groupIDs[2]}, - Action: types.PolicyTrafficActionAccept, + Enabled: true, + Sources: []string{"ap-grpA"}, + Destinations: []string{"ap-grpB"}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, }, }, }, true) require.NoError(t, err) - result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]}) - count := 0 - for _, id := range result { - if id == peerIDs[0] { - count++ + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + result := manager.resolveAffectedPeersForPeerChanges(ctx, manager.Store, accountID, []string{peer1.ID}) + assert.ElementsMatch(t, []string{peer1.ID, peer2.ID}, result) + + // Adding peer3 to grpA makes it part of the policy, so all 3 peers get updated + t.Run("group change updates all peers in policy groups", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{ + ID: "ap-grpA", + Name: "AP-A", + Peers: []string{peer1.ID, peer3.ID}, + }) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") } - } - assert.Equal(t, 1, count, "peer0 should appear exactly once") + }) + + _ = updMsg1 + _ = updMsg2 + _ = updMsg3 } -func TestPolicyReferencesGroups(t *testing.T) { - policy := &types.Policy{ +func TestAffectedPeers_UnlinkedGroupChange_NoUpdates(t *testing.T) { + manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t) + ctx := context.Background() + + result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]}) + assert.Empty(t, result) +} + +// TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate verifies that creating/deleting a +// policy only sends updates to peers in the policy's groups, not to unrelated peers. +func TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "pol-grpA", Name: "Pol-A", Peers: []string{peer1.ID}}, + {ID: "pol-grpB", Name: "Pol-B", Peers: []string{peer2.ID}}, + {ID: "pol-grpC", Name: "Pol-C", Peers: []string{peer3.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // Create policy linking only peer1 (grpA) <-> peer2 (grpB). Peer3 should not receive update. + t.Run("create policy only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + _, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{ + Enabled: true, + Rules: []*types.PolicyRule{ + { + Enabled: true, + Sources: []string{"pol-grpA"}, + Destinations: []string{"pol-grpB"}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, + }, + }, + }, true) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_RouteChange_UnrelatedPeerNoUpdate verifies that creating a route +// only sends updates to peers in the route's groups, not to unrelated peers. +func TestAffectedPeers_RouteChange_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "rt-grpA", Name: "Rt-A", Peers: []string{peer1.ID}}, + {ID: "rt-grpB", Name: "Rt-B", Peers: []string{peer2.ID}}, + {ID: "rt-grpC", Name: "Rt-C", Peers: []string{peer3.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // Create route with peer groups grpA and distribution group grpB. Peer3 should not get update. + t.Run("create route only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + _, err := manager.CreateRoute(ctx, accountID, + netip.MustParsePrefix("10.10.0.0/24"), + route.IPv4Network, + nil, + "", + []string{"rt-grpA"}, + "test route", + "routenoaffect", + false, + 9999, + []string{"rt-grpB"}, + nil, + true, + userID, + false, + false, + ) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_NameServerChange_UnrelatedPeerNoUpdate verifies that creating a +// nameserver group only sends updates to peers in its groups, not to unrelated peers. +func TestAffectedPeers_NameServerChange_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "ns-grpA", Name: "NS-A", Peers: []string{peer1.ID}}, + {ID: "ns-grpB", Name: "NS-B", Peers: []string{peer2.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // Create NS group using only grpA. peer2 and peer3 should not get update. + t.Run("create nameserver group only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldNotReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + _, err := manager.CreateNameServerGroup(ctx, accountID, "ns-unrelated", "NS Unrelated", + []nbdns.NameServer{{ + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }}, + []string{"ns-grpA"}, + true, nil, true, userID, false, + ) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_DNSSettingsChange_UnrelatedPeerNoUpdate verifies that changing DNS +// settings only sends updates to peers in the affected groups, not to unrelated peers. +func TestAffectedPeers_DNSSettingsChange_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "dns-grpA", Name: "DNS-A", Peers: []string{peer1.ID}}, + {ID: "dns-grpB", Name: "DNS-B", Peers: []string{peer2.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // Save DNS settings that only affects grpA. peer2 and peer3 should not be affected. + t.Run("dns settings change only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldNotReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.SaveDNSSettings(ctx, accountID, userID, &types.DNSSettings{ + DisabledManagementGroups: []string{"dns-grpA"}, + }) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_UnlinkedGroupChange_NoUpdateIntegration tests the full integration: +// updating a group that is NOT referenced by any policy/route/ns/dns should not send +// updates to any peer. +func TestAffectedPeers_UnlinkedGroupChange_NoUpdateIntegration(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + err = manager.CreateGroup(ctx, accountID, userID, &types.Group{ + ID: "unlinked-grp", + Name: "Unlinked", + Peers: []string{peer1.ID}, + }) + require.NoError(t, err) + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + t.Run("updating unlinked group sends no peer updates", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldNotReceiveUpdate(t, updMsg1) + peerShouldNotReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{ + ID: "unlinked-grp", + Name: "Unlinked", + Peers: []string{peer1.ID, peer2.ID}, + }) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_NetworkRouter_UnrelatedPeerNoUpdate verifies that when a network +// router is added with specific peer groups, only peers in those groups (and policy +// sources for resources) get updates. Unrelated peers should not. +func TestAffectedPeers_NetworkRouter_UnrelatedPeerNoUpdate(t *testing.T) { + // Use custom setup: delete default policy BEFORE adding peers so that + // AddPeer's BufferUpdateAffectedPeers finds no affected peers and + // doesn't schedule async updates that race with the test. + manager, updateManager, err := createManager(t) + require.NoError(t, err) + + ctx := context.Background() + + account, err := createAccount(manager, "nr_test_account", userID, "") + require.NoError(t, err) + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + setupKey, err := manager.CreateSetupKey(ctx, accountID, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false) + require.NoError(t, err) + + peer1 := addPeerToAccount(t, manager, accountID, setupKey.Key) + peer2 := addPeerToAccount(t, manager, accountID, setupKey.Key) + peer3 := addPeerToAccount(t, manager, accountID, setupKey.Key) + + for _, g := range []*types.Group{ + {ID: "nr-grpA", Name: "NR-A", Peers: []string{peer1.ID}}, + {ID: "nr-grpB", Name: "NR-B", Peers: []string{peer2.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + net1 := &networkTypes.Network{ + ID: "nr-net-test", + AccountID: accountID, + Name: "nr-test-network", + } + err = manager.Store.SaveNetwork(ctx, net1) + require.NoError(t, err) + + err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{ + ID: "nr-router-test", + NetworkID: net1.ID, + AccountID: accountID, + PeerGroups: []string{"nr-grpA"}, + }) + require.NoError(t, err) + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // When the group linked to the network router changes, only peers in that + // group should be updated. Peer2 is unrelated. Peer3 is added to the + // router's group so it should also receive an update. + t.Run("network router group change only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldNotReceiveUpdate(t, updMsg2) + peerShouldReceiveUpdate(t, updMsg3) + close(done) + }() + + // Updating the group linked to router should affect peer1 and peer3 (now in nr-grpA). + err = manager.UpdateGroup(ctx, accountID, userID, &types.Group{ + ID: "nr-grpA", + Name: "NR-A", + Peers: []string{peer1.ID, peer3.ID}, + }) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +// TestAffectedPeers_MultipleIsolatedEntities_OnlyLinkedPeersUpdated creates multiple +// isolated entities (policy for peer1<->peer2, route for peer3) and verifies that +// changing one entity's groups only affects its peers. +func TestAffectedPeers_MultipleIsolatedEntities_OnlyLinkedPeersUpdated(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + for _, g := range []*types.Group{ + {ID: "iso-grpA", Name: "ISO-A", Peers: []string{peer1.ID}}, + {ID: "iso-grpB", Name: "ISO-B", Peers: []string{peer2.ID}}, + {ID: "iso-grpC", Name: "ISO-C", Peers: []string{peer3.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) + } + + // Policy: peer1 <-> peer2 + _, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{ + Enabled: true, Rules: []*types.PolicyRule{ { - Sources: []string{"g1", "g2"}, - Destinations: []string{"g3"}, + Enabled: true, + Sources: []string{"iso-grpA"}, + Destinations: []string{"iso-grpB"}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, }, }, - } + }, true) + require.NoError(t, err) - tests := []struct { - name string - groupSet map[string]struct{} - want bool - }{ - {"matches source", map[string]struct{}{"g1": {}}, true}, - {"matches destination", map[string]struct{}{"g3": {}}, true}, - {"no match", map[string]struct{}{"g4": {}}, false}, - {"empty set", map[string]struct{}{}, false}, - } + // Route: only peer3's group as distribution group + _, err = manager.CreateRoute(ctx, accountID, + netip.MustParsePrefix("10.20.0.0/24"), + route.IPv4Network, + nil, + "", + []string{"iso-grpC"}, + "isolated route", + "isonet2", + false, + 9999, + []string{"iso-grpC"}, + nil, + true, + userID, + false, + false, + ) + require.NoError(t, err) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := policyReferencesGroups(policy, tt.groupSet) - assert.Equal(t, tt.want, got) + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + // Updating policy group (iso-grpA) should affect peer1+peer2 but NOT peer3 + t.Run("policy group change does not affect route-only peer", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{ + ID: "iso-grpA", + Name: "ISO-A-updated", + Peers: []string{peer1.ID}, }) - } + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) } -func TestRouteReferencesGroups(t *testing.T) { - r := &route.Route{ - Groups: []string{"g1"}, - PeerGroups: []string{"g2"}, - AccessControlGroups: []string{"g3"}, +// TestAffectedPeers_DeleteRoute_UnrelatedPeerNoUpdate verifies that deleting a route +// only sends updates to peers in the route's groups. +func TestAffectedPeers_DeleteRoute_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) } - tests := []struct { - name string - groupSet map[string]struct{} - want bool - }{ - {"matches groups", map[string]struct{}{"g1": {}}, true}, - {"matches peerGroups", map[string]struct{}{"g2": {}}, true}, - {"matches accessControl", map[string]struct{}{"g3": {}}, true}, - {"no match", map[string]struct{}{"g4": {}}, false}, + for _, g := range []*types.Group{ + {ID: "del-rt-grpA", Name: "Del-Rt-A", Peers: []string{peer1.ID}}, + {ID: "del-rt-grpB", Name: "Del-Rt-B", Peers: []string{peer2.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := routeReferencesGroups(r, tt.groupSet) - assert.Equal(t, tt.want, got) - }) - } + newRoute, err := manager.CreateRoute(ctx, accountID, + netip.MustParsePrefix("10.30.0.0/24"), + route.IPv4Network, + nil, + "", + []string{"del-rt-grpA"}, + "deletable route", + "delnet", + false, + 9999, + []string{"del-rt-grpB"}, + nil, + true, + userID, + false, + false, + ) + require.NoError(t, err) + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + t.Run("delete route only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.DeleteRoute(ctx, accountID, newRoute.ID, userID) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) } -func TestRouterReferencesGroups(t *testing.T) { - router := &routerTypes.NetworkRouter{ - PeerGroups: []string{"g1", "g2"}, +// TestAffectedPeers_DeletePolicy_UnrelatedPeerNoUpdate verifies that deleting a policy +// only sends updates to peers in the policy's groups. +func TestAffectedPeers_DeletePolicy_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) } - tests := []struct { - name string - groupSet map[string]struct{} - want bool - }{ - {"matches", map[string]struct{}{"g1": {}}, true}, - {"no match", map[string]struct{}{"g3": {}}, false}, + for _, g := range []*types.Group{ + {ID: "del-pol-grpA", Name: "Del-Pol-A", Peers: []string{peer1.ID}}, + {ID: "del-pol-grpB", Name: "Del-Pol-B", Peers: []string{peer2.ID}}, + } { + err := manager.CreateGroup(ctx, accountID, userID, g) + require.NoError(t, err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := routerReferencesGroups(router, tt.groupSet) - assert.Equal(t, tt.want, got) - }) - } + policy, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{ + Enabled: true, + Rules: []*types.PolicyRule{ + { + Enabled: true, + Sources: []string{"del-pol-grpA"}, + Destinations: []string{"del-pol-grpB"}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, + }, + }, + }, true) + require.NoError(t, err) + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + t.Run("delete policy only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.DeletePolicy(ctx, accountID, policy.ID, userID) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) } -func addPeerToAccount(t *testing.T, manager *DefaultAccountManager, accountID, setupKeyKey string) *nbpeer.Peer { +// TestAffectedPeers_DeleteNameServer_UnrelatedPeerNoUpdate verifies that deleting a +// nameserver group only sends updates to peers in its groups. +func TestAffectedPeers_DeleteNameServer_UnrelatedPeerNoUpdate(t *testing.T) { + manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t) + ctx := context.Background() + accountID := account.Id + + policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID) + require.NoError(t, err) + for _, p := range policies { + err := manager.Store.DeletePolicy(ctx, accountID, p.ID) + require.NoError(t, err) + } + + err = manager.CreateGroup(ctx, accountID, userID, &types.Group{ + ID: "del-ns-grpA", + Name: "Del-NS-A", + Peers: []string{peer1.ID}, + }) + require.NoError(t, err) + + nsGroup, err := manager.CreateNameServerGroup(ctx, accountID, "del-ns", "Del NS", + []nbdns.NameServer{{ + IP: netip.MustParseAddr("8.8.4.4"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }}, + []string{"del-ns-grpA"}, + true, nil, true, userID, false, + ) + require.NoError(t, err) + + updMsg1 := updateManager.CreateChannel(ctx, peer1.ID) + updMsg2 := updateManager.CreateChannel(ctx, peer2.ID) + updMsg3 := updateManager.CreateChannel(ctx, peer3.ID) + t.Cleanup(func() { + updateManager.CloseChannel(ctx, peer1.ID) + updateManager.CloseChannel(ctx, peer2.ID) + updateManager.CloseChannel(ctx, peer3.ID) + }) + + t.Run("delete nameserver group only affects linked peers", func(t *testing.T) { + done := make(chan struct{}) + go func() { + peerShouldReceiveUpdate(t, updMsg1) + peerShouldNotReceiveUpdate(t, updMsg2) + peerShouldNotReceiveUpdate(t, updMsg3) + close(done) + }() + + err := manager.DeleteNameServerGroup(ctx, accountID, nsGroup.ID, userID) + assert.NoError(t, err) + + select { + case <-done: + case <-time.After(peerUpdateTimeout): + t.Error("timeout") + } + }) +} + +func addPeerToAccount(t *testing.T, manager *DefaultAccountManager, _, setupKeyKey string) *nbpeer.Peer { t.Helper() key, err := wgtypes.GeneratePrivateKey()