github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/p2p/profile_service_test.go (about)

     1  package p2p
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	peer "github.com/libp2p/go-libp2p-core/peer"
    12  	"github.com/qri-io/qri/event"
    13  	p2ptest "github.com/qri-io/qri/p2p/test"
    14  	"github.com/qri-io/qri/profile"
    15  )
    16  
    17  // Hello fellow devs. Let my pain be your comfort and plz use my notes here
    18  // as a guide on how to not pull out your hair when testing the p2p connections
    19  // things to note:
    20  //   - once a libp2p connection has been established, the QriProfileService will
    21  //   check if that remote peers speaks qri. If it does, it will protect that
    22  //   connection, add it to a list of current qri connections, and ask that peer
    23  //   for it's qri profile. It will then emit a `event.ETP2PQriPeerConnected` event
    24  //   - if a libp2p connection has been broken, aka disconnected, the QriProfile-
    25  //   Service will remove that connection from the current qri connections and
    26  //   emit a `event.ETP2PQriPeerDisconnected` event
    27  //   - we can use these events to coordinate on
    28  //   - QriNodes have a Discovery process that the peers use to find each other
    29  //   in the "wild". This is why you may see connections to peers that you haven't
    30  //   explicitly connected to. When this is happening locally, it may happen so
    31  //   quickly that the connections can collide causing errors. Unless you are
    32  //   specifically testing that the peers can find themselves on their own, it
    33  //   may be best to disable Discovery by calling `node.Discovery.Close()`
    34  //   - when coordinating on events, ensure that you are using a mutex lock to
    35  //   prevent any race conditions
    36  //   - use channels to wait for all connections or disconnections to occur. a
    37  //   closed channel never blocks!
    38  
    39  func TestQriProfileService(t *testing.T) {
    40  	ctx := context.Background()
    41  	numNodes := 5 // number of nodes we want (besides the main test node) in this test
    42  	// create a network of connected nodes
    43  	factory := p2ptest.NewTestNodeFactory(NewTestableQriNode)
    44  	testPeers, err := p2ptest.NewTestNetwork(ctx, factory, numNodes)
    45  	if err != nil {
    46  		t.Fatalf("error creating network: %s", err.Error())
    47  	}
    48  
    49  	// Convert from test nodes to non-test nodes.
    50  	nodes := make([]*QriNode, len(testPeers))
    51  	for i, node := range testPeers {
    52  		nodes[i] = node.(*QriNode)
    53  		// closing the discovery process prevents situations where another peer finds
    54  		// our node in the wild and attempts to connect to it while we are trying
    55  		// to connect to that node at the same time. This causes a dial failure.
    56  		nodes[i].Discovery.Close()
    57  	}
    58  
    59  	expectedPeers := peer.IDSlice{}
    60  	for _, node := range nodes {
    61  		expectedPeers = append(expectedPeers, node.host.ID())
    62  	}
    63  	sort.Sort(peer.IDSlice(expectedPeers))
    64  	t.Logf("expected nodes: %v", expectedPeers)
    65  
    66  	// set up bus & listener for `ETP2PQriPeerConnected` events
    67  	// this is how we will be sure that all the nodes have exchanged qri profiles
    68  	// before moving forward
    69  	bus := event.NewBus(ctx)
    70  	qriPeerConnWaitCh := make(chan struct{})
    71  	connectedPeersMu := sync.Mutex{}
    72  	connectedPeers := peer.IDSlice{}
    73  
    74  	disconnectedPeers := peer.IDSlice{}
    75  	disconnectedPeersMu := sync.Mutex{}
    76  	disconnectsCh := make(chan struct{})
    77  
    78  	node := &QriNode{}
    79  
    80  	unexpectedPeers := peer.IDSlice{}
    81  
    82  	watchP2PQriEvents := func(_ context.Context, e event.Event) error {
    83  		pro, ok := e.Payload.(*profile.Profile)
    84  		if !ok {
    85  			t.Error("payload for event.ETP2PQriPeerConnected not a *profile.Profile as expected")
    86  			return fmt.Errorf("payload for event.ETP2PQriPeerConnected not a *profile.Profile as expected")
    87  		}
    88  		if pro == nil {
    89  			t.Error("error: event payload is a nil profile")
    90  			return fmt.Errorf("err: event payload is a nil profile")
    91  		}
    92  		pid := pro.PeerIDs[0]
    93  		expectedPeer := false
    94  		for _, id := range expectedPeers {
    95  			if pid == id {
    96  				expectedPeer = true
    97  				break
    98  			}
    99  		}
   100  		if !expectedPeer {
   101  			t.Logf("peer %q event occurred, but not an expected peer", pid)
   102  			unexpectedPeers = append(unexpectedPeers, pid)
   103  			return nil
   104  		}
   105  		if e.Type == event.ETP2PQriPeerConnected {
   106  			connectedPeersMu.Lock()
   107  			defer connectedPeersMu.Unlock()
   108  			t.Log("Qri Peer Connected: ", pid)
   109  			for _, id := range connectedPeers {
   110  				if pid == id {
   111  					t.Logf("peer %q has already been connected", pid)
   112  					return nil
   113  				}
   114  			}
   115  			connectedPeers = append(connectedPeers, pid)
   116  			if len(connectedPeers) == numNodes {
   117  				close(qriPeerConnWaitCh)
   118  			}
   119  		}
   120  		if e.Type == event.ETP2PQriPeerDisconnected {
   121  			disconnectedPeersMu.Lock()
   122  			defer disconnectedPeersMu.Unlock()
   123  			t.Log("Qri Peer Disconnected: ", pid)
   124  			for _, id := range disconnectedPeers {
   125  				if pid == id {
   126  					t.Logf("peer %q has already been disconnected", pid)
   127  					return nil
   128  				}
   129  			}
   130  			disconnectedPeers = append(disconnectedPeers, pid)
   131  			if len(disconnectedPeers) == numNodes {
   132  				close(disconnectsCh)
   133  			}
   134  		}
   135  		return nil
   136  	}
   137  	bus.SubscribeTypes(watchP2PQriEvents, event.ETP2PQriPeerConnected, event.ETP2PQriPeerDisconnected)
   138  
   139  	// create a new, disconnected node
   140  	testnode, err := p2ptest.NewNodeWithBus(ctx, factory, bus)
   141  	if err != nil {
   142  		t.Fatal(err)
   143  	}
   144  	node = testnode.(*QriNode)
   145  	// closing the discovery process prevents situations where another peer finds
   146  	// our node in the wild and attempts to connect to it while we are trying
   147  	// to connect to that node at the same time. This causes a dial failure.
   148  	node.Discovery.Close()
   149  	defer node.GoOffline()
   150  	t.Log("node id: ", node.host.ID())
   151  
   152  	connectedPeers = node.qis.ConnectedQriPeers()
   153  	if len(connectedPeers) != 0 {
   154  		t.Errorf("expected 0 peers to be connected to the isolated node, but got %d connected qri peers instead", len(connectedPeers))
   155  	}
   156  
   157  	// explicitly connect the main test node to each other node in the network
   158  	for _, groupNode := range nodes {
   159  		addrInfo := peer.AddrInfo{
   160  			ID:    groupNode.Host().ID(),
   161  			Addrs: groupNode.Host().Addrs(),
   162  		}
   163  		err := node.Host().Connect(context.Background(), addrInfo)
   164  		if err != nil {
   165  			t.Logf("error connecting to peer %q: %s", groupNode.Host().ID(), err)
   166  		}
   167  	}
   168  
   169  	// wait for all nodes to upgrade to qri peers
   170  	<-qriPeerConnWaitCh
   171  
   172  	// get a list of connected peers, according to the QriProfileService
   173  	connectedPeers = node.qis.ConnectedQriPeers()
   174  	sort.Sort(peer.IDSlice(connectedPeers))
   175  
   176  	// ensure the lists are the same
   177  	if len(connectedPeers) == 0 {
   178  		t.Errorf("error exchange qri identities: expected number of connected peers to be %d, got %d", len(expectedPeers), len(connectedPeers))
   179  	}
   180  
   181  	if len(unexpectedPeers) != 0 {
   182  		t.Errorf("unexpected peers found: %v", unexpectedPeers)
   183  		for _, pid := range unexpectedPeers {
   184  			protocols, err := node.host.Peerstore().GetProtocols(pid)
   185  			if err != nil {
   186  				t.Errorf("error getting peer %q protocols: %s", pid, err)
   187  			} else {
   188  				t.Logf("peer %q speaks protocols: %v", pid, protocols)
   189  			}
   190  		}
   191  	}
   192  
   193  	if len(expectedPeers) != len(connectedPeers) {
   194  		t.Errorf("expected list of connected peers different then the given list of connected peers: \n  expected: %v\n  got: %v", expectedPeers, connectedPeers)
   195  		for _, peer := range connectedPeers {
   196  			pro, err := node.Repo.Profiles().PeerProfile(ctx, peer)
   197  			if err != nil {
   198  				t.Errorf("error getting peer %q profile: %s", peer, err)
   199  			}
   200  			t.Logf("%s, %v", peer, pro)
   201  		}
   202  		return
   203  	}
   204  	different := false
   205  	for i, pid := range expectedPeers {
   206  		if pid != connectedPeers[i] {
   207  			different = true
   208  			break
   209  		}
   210  	}
   211  	if different {
   212  		t.Errorf("expected list of connected peers different then the given list of connected peers: \n  expected: %v\n  got: %v", expectedPeers, connectedPeers)
   213  	}
   214  
   215  	// check that each connection is protected, and the we have a profile for each
   216  	for _, id := range connectedPeers {
   217  		if protected := node.host.ConnManager().IsProtected(id, qriSupportKey); !protected {
   218  			t.Errorf("expected peer %q to have a protected connection, but it does not", id)
   219  		}
   220  		pro := node.qis.ConnectedPeerProfile(id)
   221  		if pro == nil {
   222  			t.Errorf("expected to have peer %q's qri profile, but wasn't able to receive it", id)
   223  		}
   224  	}
   225  
   226  	// disconnect each node
   227  	for _, node := range nodes {
   228  		node.GoOffline()
   229  	}
   230  
   231  	<-disconnectsCh
   232  	// get a list of connected qri peers according to the QriProfileService
   233  	connectedPeers = node.qis.ConnectedQriPeers()
   234  
   235  	if len(connectedPeers) != 0 {
   236  		t.Errorf("error with catching disconnects, expected 0 remaining connections, got %d to peers %v", len(connectedPeers), connectedPeers)
   237  	}
   238  }
   239  
   240  func TestDiscoveryConnection(t *testing.T) {
   241  	t.Skip("enable this test to see if nodes will find each other on your machine using their Discovery methods without getting orders to explicitily connect")
   242  	ctx := context.Background()
   243  	// create a network of connected nodes
   244  	factory := p2ptest.NewTestNodeFactory(NewTestableQriNode)
   245  	// create a new, disconnected node
   246  	busA := event.NewBus(ctx)
   247  
   248  	watchP2PQriEvents := func(_ context.Context, e event.Event) error {
   249  		pro, ok := e.Payload.(*profile.Profile)
   250  		if !ok {
   251  			t.Error("payload for event.ETP2PQriPeerConnected not a *profile.Profile as expected")
   252  			return fmt.Errorf("payload for event.ETP2PQriPeerConnected not a *profile.Profile as expected")
   253  		}
   254  		pid := pro.PeerIDs[0]
   255  		if e.Type == event.ETP2PQriPeerConnected {
   256  			t.Log("Qri Peer Connected: ", pid)
   257  		}
   258  		if e.Type == event.ETP2PQriPeerDisconnected {
   259  			t.Log("Qri Peer Disconnected: ", pid)
   260  		}
   261  		return nil
   262  	}
   263  	busA.SubscribeTypes(watchP2PQriEvents, event.ETP2PQriPeerConnected, event.ETP2PQriPeerDisconnected)
   264  
   265  	testNodeA, err := p2ptest.NewNodeWithBus(ctx, factory, busA)
   266  	if err != nil {
   267  		t.Error(err.Error())
   268  		return
   269  	}
   270  	nodeA := testNodeA.(*QriNode)
   271  
   272  	testNodeB, err := p2ptest.NewNodeWithBus(ctx, factory, event.NilBus)
   273  	if err != nil {
   274  		t.Error(err.Error())
   275  		return
   276  	}
   277  	nodeB := testNodeB.(*QriNode)
   278  
   279  	time.Sleep(time.Second * 30)
   280  
   281  	t.Errorf("nodeA's connections: %v", nodeA.ConnectedQriPeerIDs())
   282  	t.Errorf("nodeB's connections: %v", nodeB.ConnectedQriPeerIDs())
   283  	nodeA.GoOffline()
   284  	nodeB.GoOffline()
   285  }