diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f42839..a7d7ed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Added - Added extra comments on main.go +- Remove backoff_test.go and associated backoff tests +- Created node_test, write tests for CalculateNextBackoff, AddJitter, GetPeerId, LoadOrCreateIdentity, hasBootstrapConnections ### Changed diff --git a/pkg/node/backoff_test.go b/pkg/node/backoff_test.go deleted file mode 100644 index e26d79f..0000000 --- a/pkg/node/backoff_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package node - -import ( - "testing" - "time" -) - -func TestCalculateNextBackoff(t *testing.T) { - if got := calculateNextBackoff(10 * time.Second); got <= 10*time.Second || got > 15*time.Second { - t.Fatalf("unexpected next: %v", got) - } - if got := calculateNextBackoff(10 * time.Minute); got != 10*time.Minute { - t.Fatalf("cap not applied: %v", got) - } -} - -func TestAddJitter(t *testing.T) { - base := 10 * time.Second - min := base - time.Duration(0.2*float64(base)) - max := base + time.Duration(0.2*float64(base)) - for i := 0; i < 100; i++ { - got := addJitter(base) - if got < time.Second || got < min || got > max { - t.Fatalf("jitter out of range: %v", got) - } - } -} diff --git a/pkg/node/node_test.go b/pkg/node/node_test.go new file mode 100644 index 0000000..d25e20b --- /dev/null +++ b/pkg/node/node_test.go @@ -0,0 +1,311 @@ +package node + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "git.debros.io/DeBros/network/pkg/config" + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +func TestCalculateNextBackoff(t *testing.T) { + if got := calculateNextBackoff(10 * time.Second); got <= 10*time.Second || got > 15*time.Second { + t.Fatalf("unexpected next: %v", got) + } + if got := calculateNextBackoff(10 * time.Minute); got != 10*time.Minute { + t.Fatalf("cap not applied: %v", got) + } +} + +func TestAddJitter(t *testing.T) { + base := 10 * time.Second + min := base - time.Duration(0.2*float64(base)) + max := base + time.Duration(0.2*float64(base)) + for i := 0; i < 100; i++ { + got := addJitter(base) + if got < time.Second || got < min || got > max { + t.Fatalf("jitter out of range: %v", got) + } + } +} + +func TestGetPeerId_WhenNoHost(t *testing.T) { + n := &Node{} + if id := n.GetPeerID(); id != "" { + t.Fatalf("GetPeerID() = %q; want empty string when host is nil", id) + } +} + +func TestLoadOrCreateIdentity(t *testing.T) { + t.Run("first run creates file with correct perms and round-trips", func(t *testing.T) { + tempDir := t.TempDir() + cfg := &config.Config{} + cfg.Node.DataDir = tempDir + + n, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode() error: %v", err) + } + + priv, err := n.loadOrCreateIdentity() + if err != nil { + t.Fatalf("loadOrCreateIdentity() error: %v", err) + } + if priv == nil { + t.Fatalf("returned private key is nil") + } + + identityFile := filepath.Join(tempDir, "identity.key") + + // File exists + fi, err := os.Stat(identityFile) + if err != nil { + t.Fatalf("identity file not created: %v", err) + } + + // Permissions are 0600 + if got := fi.Mode().Perm(); got != 0o600 { + t.Fatalf("identity file permissions are incorrect: %v", got) + } + + // Saved key can be unmarshaled and matches returned key + data, err := os.ReadFile(identityFile) + if err != nil { + t.Fatalf("failed to read identity file: %v", err) + } + priv2, err := crypto.UnmarshalPrivateKey(data) + if err != nil { + t.Fatalf("UnmarshalPrivateKey: %v", err) + } + + b1, err := crypto.MarshalPrivateKey(priv) + if err != nil { + t.Fatalf("MarshalPrivateKey(priv): %v", err) + } + b2, err := crypto.MarshalPrivateKey(priv2) + if err != nil { + t.Fatalf("MarshalPrivateKey(priv2): %v", err) + } + if !bytes.Equal(b1, b2) { + t.Fatalf("saved key differs from returned key") + } + }) + + t.Run("second run returns same identity", func(t *testing.T) { + tempDir := t.TempDir() + cfg := &config.Config{} + cfg.Node.DataDir = tempDir + + // First run + n1, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode() error: %v", err) + } + priv1, err := n1.loadOrCreateIdentity() + if err != nil { + t.Fatalf("loadOrCreateIdentity(first) error: %v", err) + } + + // Second run with a new Node pointing to the same dir + n2, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode() error: %v", err) + } + priv2, err := n2.loadOrCreateIdentity() + if err != nil { + t.Fatalf("loadOrCreateIdentity(second) error: %v", err) + } + + // Compare marshaled keys + b1, err := crypto.MarshalPrivateKey(priv1) + if err != nil { + t.Fatalf("MarshalPrivateKey(priv1): %v", err) + } + b2, err := crypto.MarshalPrivateKey(priv2) + if err != nil { + t.Fatalf("MarshalPrivateKey(priv2): %v", err) + } + if !bytes.Equal(b1, b2) { + t.Fatalf("second run did not return the same identity") + } + }) +} + +func TestHashBootstrapConnections(t *testing.T) { + cfg := &config.Config{} + + n, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode() error: %v", err) + } + + // Assert: Does not have bootstrap connections + conns := n.hasBootstrapConnections() + if conns != false { + t.Fatalf("expected false, got %v", conns) + } + + // Variation: Fresh libp2p still zero connections + h, err := libp2p.New() + if err != nil { + t.Fatalf("libp2p.New() error: %v", err) + } + defer h.Close() + + n.host = h + conns = n.hasBootstrapConnections() + if conns != false { + t.Fatalf("expected false, got %v", conns) + } + + // Assert: Return true if connected to at least one bootstrap peer + t.Run("returns true when connected to at least one configured bootstrap peer", func(t *testing.T) { + // Fresh node and config + cfg := &config.Config{} + n2, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode: %v", err) + } + + // Create two hosts (A and B) listening on localhost TCP + hA, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p.New (A): %v", err) + } + defer hA.Close() + + hB, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p.New (B): %v", err) + } + defer hB.Close() + + // Build B's bootstrap multiaddr: /p2p/ + var base multiaddr.Multiaddr + for _, a := range hB.Addrs() { + if strings.Contains(a.String(), "/tcp/") { + base = a + break + } + } + if base == nil { + t.Skip("no TCP listen address for host B") + } + pidMA, err := multiaddr.NewMultiaddr("/p2p/" + hB.ID().String()) + if err != nil { + t.Fatalf("NewMultiaddr(/p2p/): %v", err) + } + bootstrap := base.Encapsulate(pidMA).String() + + // Configure node A with B as a bootstrap peer + n2.host = hA + n2.config.Discovery.BootstrapPeers = []string{bootstrap} + + // Connect A -> B + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := hA.Connect(ctx, peer.AddrInfo{ID: hB.ID(), Addrs: hB.Addrs()}); err != nil { + t.Fatalf("connect A->B: %v", err) + } + + // Wait until connected + deadline := time.Now().Add(2 * time.Second) + for { + if hA.Network().Connectedness(hB.ID()) == network.Connected { + break + } + if time.Now().After(deadline) { + t.Fatalf("A not connected to B after timeout") + } + time.Sleep(10 * time.Millisecond) + } + + // Assert: hasBootstrapConnections returns true + if !n2.hasBootstrapConnections() { + t.Fatalf("expected hasBootstrapConnections() to be true") + } + }) + + t.Run("returns false when connected peers are not in the bootstrap list", func(t *testing.T) { + // Fresh node and config + cfg := &config.Config{} + n2, err := NewNode(cfg) + if err != nil { + t.Fatalf("NewNode: %v", err) + } + + // Create three hosts (A, B, C) listening on localhost TCP + hA, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p.New (A): %v", err) + } + defer hA.Close() + + hB, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p.New (B): %v", err) + } + defer hB.Close() + + hC, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p.New (C): %v", err) + } + defer hC.Close() + + // Build C's bootstrap multiaddr: /p2p/ + var baseC multiaddr.Multiaddr + for _, a := range hC.Addrs() { + if strings.Contains(a.String(), "/tcp/") { + baseC = a + break + } + } + if baseC == nil { + t.Skip("no TCP listen address for host C") + } + pidC, err := multiaddr.NewMultiaddr("/p2p/" + hC.ID().String()) + if err != nil { + t.Fatalf("NewMultiaddr(/p2p/): %v", err) + } + bootstrapC := baseC.Encapsulate(pidC).String() + + // Configure node A with ONLY C as a bootstrap peer + n2.host = hA + n2.config.Discovery.BootstrapPeers = []string{bootstrapC} + + // Connect A -> B (but C is in the bootstrap list, not B) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := hA.Connect(ctx, peer.AddrInfo{ID: hB.ID(), Addrs: hB.Addrs()}); err != nil { + t.Fatalf("connect A->B: %v", err) + } + + // Wait until A is connected to B + deadline := time.Now().Add(2 * time.Second) + for { + if hA.Network().Connectedness(hB.ID()) == network.Connected { + break + } + if time.Now().After(deadline) { + t.Fatalf("A not connected to B after timeout") + } + time.Sleep(10 * time.Millisecond) + } + + // Assert: hasBootstrapConnections should be false (connected peer is not in bootstrap list) + if n2.hasBootstrapConnections() { + t.Fatalf("expected hasBootstrapConnections() to be false") + } + }) + +}