All checks were successful
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 34s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 38s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 37s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 32s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 35s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 34s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 37s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 42s
Go Build Multi-Platform / Create Release (push) Has been skipped
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 1m29s
Go Revive Lint / lint (push) Successful in 47s
Run Gosec / tests (push) Successful in 1m3s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 44s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 42s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m5s
297 lines
7.4 KiB
Go
297 lines
7.4 KiB
Go
package interfaces
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
|
)
|
|
|
|
func TestNewAutoInterface(t *testing.T) {
|
|
t.Run("DefaultConfig", func(t *testing.T) {
|
|
config := &common.InterfaceConfig{Enabled: true}
|
|
ai, err := NewAutoInterface("autoDefault", config)
|
|
if err != nil {
|
|
t.Fatalf("NewAutoInterface failed with default config: %v", err)
|
|
}
|
|
if ai == nil {
|
|
t.Fatal("NewAutoInterface returned nil with default config")
|
|
}
|
|
|
|
if ai.GetName() != "autoDefault" {
|
|
t.Errorf("GetName() = %s; want autoDefault", ai.GetName())
|
|
}
|
|
if ai.GetType() != common.IF_TYPE_AUTO {
|
|
t.Errorf("GetType() = %v; want %v", ai.GetType(), common.IF_TYPE_AUTO)
|
|
}
|
|
if ai.discoveryPort != DEFAULT_DISCOVERY_PORT {
|
|
t.Errorf("discoveryPort = %d; want %d", ai.discoveryPort, DEFAULT_DISCOVERY_PORT)
|
|
}
|
|
if ai.dataPort != DEFAULT_DATA_PORT {
|
|
t.Errorf("dataPort = %d; want %d", ai.dataPort, DEFAULT_DATA_PORT)
|
|
}
|
|
if string(ai.groupID) != "reticulum" {
|
|
t.Errorf("groupID = %s; want reticulum", string(ai.groupID))
|
|
}
|
|
if ai.discoveryScope != SCOPE_LINK {
|
|
t.Errorf("discoveryScope = %s; want %s", ai.discoveryScope, SCOPE_LINK)
|
|
}
|
|
if len(ai.peers) != 0 {
|
|
t.Errorf("peers map not empty initially")
|
|
}
|
|
})
|
|
|
|
t.Run("CustomConfig", func(t *testing.T) {
|
|
config := &common.InterfaceConfig{
|
|
Enabled: true,
|
|
DiscoveryPort: 12345,
|
|
DataPort: 54321,
|
|
GroupID: "customGroup",
|
|
}
|
|
ai, err := NewAutoInterface("autoCustom", config)
|
|
if err != nil {
|
|
t.Fatalf("NewAutoInterface failed with custom config: %v", err)
|
|
}
|
|
if ai == nil {
|
|
t.Fatal("NewAutoInterface returned nil with custom config")
|
|
}
|
|
|
|
if ai.discoveryPort != 12345 {
|
|
t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
|
|
}
|
|
if ai.dataPort != 54321 {
|
|
t.Errorf("dataPort = %d; want 54321", ai.dataPort)
|
|
}
|
|
if string(ai.groupID) != "customGroup" {
|
|
t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
|
|
}
|
|
})
|
|
}
|
|
|
|
// mockAutoInterface embeds AutoInterface but overrides methods that start goroutines
|
|
type mockAutoInterface struct {
|
|
*AutoInterface
|
|
}
|
|
|
|
func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAutoInterface, error) {
|
|
ai, err := NewAutoInterface(name, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize maps that would normally be initialized in Start()
|
|
ai.peers = make(map[string]*Peer)
|
|
ai.linkLocalAddrs = make([]string, 0)
|
|
ai.adoptedInterfaces = make(map[string]*AdoptedInterface)
|
|
ai.interfaceServers = make(map[string]*net.UDPConn)
|
|
ai.discoveryServers = make(map[string]*net.UDPConn)
|
|
ai.multicastEchoes = make(map[string]time.Time)
|
|
ai.timedOutInterfaces = make(map[string]time.Time)
|
|
|
|
return &mockAutoInterface{AutoInterface: ai}, nil
|
|
}
|
|
|
|
func (m *mockAutoInterface) Start() error {
|
|
// Don't start any goroutines
|
|
return nil
|
|
}
|
|
|
|
func (m *mockAutoInterface) Stop() error {
|
|
// Don't try to close connections that were never opened
|
|
return nil
|
|
}
|
|
|
|
// mockHandlePeerAnnounce is a test-only method that doesn't handle its own locking
|
|
func (m *mockAutoInterface) mockHandlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
|
|
peerAddr := addr.IP.String() + "%" + addr.Zone
|
|
|
|
for _, localAddr := range m.linkLocalAddrs {
|
|
if peerAddr == localAddr {
|
|
m.multicastEchoes[ifaceName] = time.Now()
|
|
return
|
|
}
|
|
}
|
|
|
|
if _, exists := m.peers[peerAddr]; !exists {
|
|
m.peers[peerAddr] = &Peer{
|
|
ifaceName: ifaceName,
|
|
lastHeard: time.Now(),
|
|
}
|
|
} else {
|
|
m.peers[peerAddr].lastHeard = time.Now()
|
|
}
|
|
}
|
|
|
|
func TestAutoInterfacePeerManagement(t *testing.T) {
|
|
// Use a shorter timeout for testing
|
|
testTimeout := 100 * time.Millisecond
|
|
|
|
config := &common.InterfaceConfig{Enabled: true}
|
|
ai, err := newMockAutoInterface("autoPeerTest", config)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create mock interface: %v", err)
|
|
}
|
|
|
|
// Create a done channel to signal goroutine cleanup
|
|
done := make(chan struct{})
|
|
|
|
// Start peer management with done channel
|
|
go func() {
|
|
ticker := time.NewTicker(testTimeout)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
ai.mutex.Lock()
|
|
now := time.Now()
|
|
for addr, peer := range ai.peers {
|
|
if now.Sub(peer.lastHeard) > testTimeout {
|
|
delete(ai.peers, addr)
|
|
}
|
|
}
|
|
ai.mutex.Unlock()
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Ensure cleanup
|
|
defer func() {
|
|
close(done)
|
|
ai.Stop()
|
|
}()
|
|
|
|
// Simulate receiving peer announces
|
|
peer1AddrStr := "fe80::1%eth0"
|
|
peer2AddrStr := "fe80::2%eth0"
|
|
localAddrStr := "fe80::aaaa%eth0" // Simulate a local address
|
|
|
|
peer1Addr := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Zone: "eth0"}
|
|
peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
|
|
localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}
|
|
|
|
// Add a simulated local address to avoid adding it as a peer
|
|
ai.mutex.Lock()
|
|
ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
|
|
ai.mutex.Unlock()
|
|
|
|
t.Run("AddPeer1", func(t *testing.T) {
|
|
ai.mutex.Lock()
|
|
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
|
ai.mutex.Unlock()
|
|
|
|
// Give a small amount of time for the peer to be processed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
ai.mutex.RLock()
|
|
count := len(ai.peers)
|
|
peer, exists := ai.peers[peer1AddrStr]
|
|
var ifaceName string
|
|
if exists {
|
|
ifaceName = peer.ifaceName
|
|
}
|
|
ai.mutex.RUnlock()
|
|
|
|
if count != 1 {
|
|
t.Fatalf("Expected 1 peer, got %d", count)
|
|
}
|
|
if !exists {
|
|
t.Fatalf("Peer %s not found in map", peer1AddrStr)
|
|
}
|
|
if ifaceName != "eth0" {
|
|
t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
|
|
}
|
|
})
|
|
|
|
t.Run("AddPeer2", func(t *testing.T) {
|
|
ai.mutex.Lock()
|
|
ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
|
|
ai.mutex.Unlock()
|
|
|
|
// Give a small amount of time for the peer to be processed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
ai.mutex.RLock()
|
|
count := len(ai.peers)
|
|
_, exists := ai.peers[peer2AddrStr]
|
|
ai.mutex.RUnlock()
|
|
|
|
if count != 2 {
|
|
t.Fatalf("Expected 2 peers, got %d", count)
|
|
}
|
|
if !exists {
|
|
t.Fatalf("Peer %s not found in map", peer2AddrStr)
|
|
}
|
|
})
|
|
|
|
t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
|
|
ai.mutex.Lock()
|
|
ai.mockHandlePeerAnnounce(localAddr, "eth0")
|
|
ai.mutex.Unlock()
|
|
|
|
// Give a small amount of time for the peer to be processed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
ai.mutex.RLock()
|
|
count := len(ai.peers)
|
|
ai.mutex.RUnlock()
|
|
|
|
if count != 2 {
|
|
t.Fatalf("Expected 2 peers after local announce, got %d", count)
|
|
}
|
|
})
|
|
|
|
t.Run("UpdatePeerTimestamp", func(t *testing.T) {
|
|
ai.mutex.RLock()
|
|
peer, exists := ai.peers[peer1AddrStr]
|
|
var initialTime time.Time
|
|
if exists {
|
|
initialTime = peer.lastHeard
|
|
}
|
|
ai.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
|
|
}
|
|
|
|
ai.mutex.Lock()
|
|
ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
|
|
ai.mutex.Unlock()
|
|
|
|
// Give a small amount of time for the peer to be processed
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
ai.mutex.RLock()
|
|
peer, exists = ai.peers[peer1AddrStr]
|
|
var updatedTime time.Time
|
|
if exists {
|
|
updatedTime = peer.lastHeard
|
|
}
|
|
ai.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
|
|
}
|
|
|
|
if !updatedTime.After(initialTime) {
|
|
t.Errorf("Peer timestamp was not updated after receiving another announce")
|
|
}
|
|
})
|
|
|
|
t.Run("PeerTimeout", func(t *testing.T) {
|
|
// Wait for peer timeout
|
|
time.Sleep(testTimeout * 2)
|
|
|
|
ai.mutex.RLock()
|
|
count := len(ai.peers)
|
|
ai.mutex.RUnlock()
|
|
|
|
if count != 0 {
|
|
t.Errorf("Expected all peers to timeout, got %d peers", count)
|
|
}
|
|
})
|
|
}
|