Files

207 lines
5.2 KiB
Go

// Package test provides helper methods, sample configurations and simulations for testing.
package test
import (
"crypto/rand"
"fmt"
"net"
"path/filepath"
"testing"
"time"
"git.clan.lol/clan/data-mesher/pkg/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
// MockResolver is a test implementation of dmnet.InterfaceResolver that returns
// predefined addresses for interface names.
type MockResolver struct {
Interfaces map[string][]*net.IPNet
}
// InterfaceAddresses returns the predefined addresses for the named interface.
func (m MockResolver) InterfaceAddresses(name string) ([]*net.IPNet, error) {
addrs, ok := m.Interfaces[name]
if !ok {
return nil, fmt.Errorf("interface %q not found", name)
}
return addrs, nil
}
// IsReachable returns true if the multiaddr's IP is within the subnet of any specified interface.
func (m MockResolver) IsReachable(addr multiaddr.Multiaddr, interfaces []string) (bool, error) {
netAddr, err := manet.ToNetAddr(addr)
if err != nil {
return false, fmt.Errorf("failed to convert multiaddr: %w", err)
}
host, _, err := net.SplitHostPort(netAddr.String())
if err != nil {
return false, fmt.Errorf("failed to split host port: %w", err)
}
ip := net.ParseIP(host)
if ip == nil {
return false, fmt.Errorf("failed to parse IP: %s", host)
}
for _, ifaceName := range interfaces {
addrs, ok := m.Interfaces[ifaceName]
if !ok {
continue
}
for _, a := range addrs {
if a.Contains(ip) {
return true, nil
}
}
}
return false, nil
}
// ListenMultiaddrs returns multiaddrs for listening based on the mock's interface addresses.
func (m MockResolver) ListenMultiaddrs(interfaces []string, port uint16) ([]multiaddr.Multiaddr, error) {
if len(interfaces) == 0 {
// return wildcard multiaddrs, no multicast or link-local
ip4, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))
if err != nil {
return nil, fmt.Errorf("failed to create IP4 multiaddr: %w", err)
}
ip6, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip6/::/tcp/%d", port))
if err != nil {
return nil, fmt.Errorf("failed to create IP6 multiaddr: %w", err)
}
return []multiaddr.Multiaddr{ip4, ip6}, nil
}
var result []multiaddr.Multiaddr
for _, name := range interfaces {
addrs, ok := m.Interfaces[name]
if !ok {
return nil, fmt.Errorf("interface %q not found", name)
}
for _, addr := range addrs {
// skip multicast and link-local addresses
if addr.IP.IsMulticast() || addr.IP.IsLinkLocalMulticast() || addr.IP.IsLinkLocalUnicast() {
continue
}
var proto string
if addr.IP.To4() != nil {
proto = "ip4"
} else {
proto = "ip6"
}
ma, err := multiaddr.NewMultiaddr(fmt.Sprintf("/%s/%s/tcp/%d", proto, addr.IP.String(), port))
if err != nil {
return nil, fmt.Errorf("failed to create multiaddr: %w", err)
}
result = append(result, ma)
}
}
return result, nil
}
// MustParseIPNet parses a CIDR string into a *net.IPNet or panics.
// Unlike net.ParseCIDR, this preserves the actual IP address (e.g., "192.168.1.1/24"
// returns an IPNet with IP=192.168.1.1, not the network address 192.168.1.0).
func MustParseIPNet(cidr string) *net.IPNet {
ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Sprintf("invalid CIDR %q: %v", cidr, err))
}
// Preserve the original IP, not the network address
ipNet.IP = ip
return ipNet
}
// Clock returns an implementation of model.Clock that always returns the provided time value when called.
func Clock(now time.Time) func() time.Time {
return func() time.Time {
return now
}
}
// GenerateBootstrapPeer generates a random peer identity and returns a peer.AddrInfo and its multiaddr string
// representation, suitable for use as a bootstrap peer in tests.
func GenerateBootstrapPeer(t *testing.T) (peer.AddrInfo, string) {
t.Helper()
key, err := crypto.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
peerID, err := key.PeerID()
if err != nil {
t.Fatal(err)
}
addr := "/ip4/127.0.0.1/tcp/7946/p2p/" + peerID.String()
ai, err := peer.AddrInfoFromString(addr)
if err != nil {
t.Fatal(err)
}
return *ai, addr
}
// GenerateIdentityKey generates an Ed25519 identity key for testing and returns both the key and the path to a
// PEM-encoded key file.
func GenerateIdentityKey(t *testing.T) (*crypto.PrivateKey, string) {
t.Helper()
key, err := crypto.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
keyPath := filepath.Join(t.TempDir(), "identity.key")
if err := key.WriteToFile(keyPath); err != nil {
t.Fatal(err)
}
return key, keyPath
}
// CertBundle holds the parsed objects and file paths for a certificate and CA public key.
type CertBundle struct {
Cert *crypto.Certificate
CAKeys []*crypto.PublicKey
CertPath string
CAPubPath string
}
// GenerateKeys generates a specified number of ED25519 private keys for testing purposes and returns them as a slice.
func GenerateKeys(t *testing.T, count int) []*crypto.PrivateKey {
t.Helper()
result := make([]*crypto.PrivateKey, count)
for i := range count {
key, err := crypto.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
result[i] = key
}
return result
}