207 lines
5.2 KiB
Go
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
|
|
}
|