mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-08 19:58:11 +00:00
On a multi-homed host the discovery layer used to walk net.Interfaces()
and pick the first non-loopback IPv4 NIC, while UPnP/SSDP bound a
wildcard UDP socket and let the kernel route the multicast send. That
meant --bind on soundtouch-web only moved the HTTP listener; the
discovery still went out whatever interface the kernel preferred (often
the wrong one on hosts where the speakers sit behind a secondary NIC).
Introduce a separate DiscoveryInterface knob:
* pkg/config: DiscoveryInterface field + DISCOVERY_INTERFACE env var.
* pkg/discovery/mdns: NewMDNSDiscoveryServiceWithInterface; the
interface resolver now honours an explicit name and validates it
has a usable IPv4 address before handing it to hashicorp/mdns.
* pkg/discovery/upnp: when an interface is configured, bind the UDP
socket's source IP to the NIC's IPv4 and call
ipv4.PacketConn.SetMulticastInterface so M-SEARCH leaves the right
NIC. Without an interface, behaviour is unchanged.
* cmd/soundtouch-web: new --interface flag (DISCOVERY_INTERFACE env)
plumbed into the config before the discovery service is built.
go.mod/go.sum reflect promoting golang.org/x/net from indirect to a
direct dependency (now imported for ipv4.PacketConn).
Refs #264.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
4.2 KiB
Go
141 lines
4.2 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewMDNSDiscoveryService(t *testing.T) {
|
|
// Test with custom timeout
|
|
service := NewMDNSDiscoveryService(10 * time.Second)
|
|
if service.timeout != 10*time.Second {
|
|
t.Errorf("Expected timeout 10s, got %v", service.timeout)
|
|
}
|
|
|
|
// Test with zero timeout (should use default)
|
|
service = NewMDNSDiscoveryService(0)
|
|
if service.timeout != defaultTimeout {
|
|
t.Errorf("Expected default timeout %v, got %v", defaultTimeout, service.timeout)
|
|
}
|
|
}
|
|
|
|
func TestMDNSDiscoverDevices(t *testing.T) {
|
|
service := NewMDNSDiscoveryService(100 * time.Millisecond)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Note: This test will attempt actual mDNS discovery
|
|
// In a real network environment, this might find actual SoundTouch devices
|
|
// In a test environment without devices, it should return an empty slice
|
|
devices, _ := service.DiscoverDevices(ctx)
|
|
|
|
// devices slice should be initialized (but might be empty)
|
|
// We don't fail on errors as they may be due to network conditions in test environment
|
|
if devices == nil {
|
|
t.Error("Expected devices slice to be initialized, got nil")
|
|
}
|
|
|
|
// If devices are found, verify they have the required fields
|
|
for _, device := range devices {
|
|
if device.Host == "" {
|
|
t.Error("Device host should not be empty")
|
|
}
|
|
|
|
if device.Port == 0 {
|
|
t.Error("Device port should not be zero")
|
|
}
|
|
|
|
if device.Name == "" {
|
|
t.Error("Device name should not be empty")
|
|
}
|
|
|
|
if device.InfoURL == "" {
|
|
t.Error("Device info URL should not be empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMDNSServiceEntryToDevice(t *testing.T) {
|
|
service := NewMDNSDiscoveryService(5 * time.Second)
|
|
|
|
// Test with nil entry
|
|
device := service.serviceEntryToDevice(nil)
|
|
if device != nil {
|
|
t.Error("Expected nil device for nil entry")
|
|
}
|
|
|
|
// Note: Testing with actual zeroconf.ServiceEntry would require
|
|
// creating mock objects or using a testing framework that can
|
|
// create proper ServiceEntry instances. For now, we test the nil case.
|
|
}
|
|
|
|
func TestMDNSDiscoveryTimeout(t *testing.T) {
|
|
service := NewMDNSDiscoveryService(100 * time.Millisecond)
|
|
|
|
start := time.Now()
|
|
ctx := context.Background()
|
|
|
|
_, err := service.DiscoverDevices(ctx)
|
|
duration := time.Since(start)
|
|
|
|
// The discovery should not take significantly longer than the timeout
|
|
// Allow some buffer for processing time
|
|
maxExpected := 200 * time.Millisecond
|
|
if duration > maxExpected {
|
|
t.Errorf("Discovery took too long: %v, expected less than %v", duration, maxExpected)
|
|
}
|
|
|
|
// We don't check for error here because mDNS discovery might succeed quickly
|
|
// or fail due to network conditions, both are acceptable in tests
|
|
_ = err
|
|
}
|
|
|
|
func TestMDNSGetIPv4InterfaceUnknownName(t *testing.T) {
|
|
service := NewMDNSDiscoveryServiceWithInterface(5*time.Second, "definitely-not-a-real-iface-xyz")
|
|
if iface := service.getIPv4Interface(); iface != nil {
|
|
t.Errorf("Expected nil for unknown interface name, got %q", iface.Name)
|
|
}
|
|
}
|
|
|
|
func TestMDNSGetIPv4InterfaceExplicitMatchesAutoPick(t *testing.T) {
|
|
auto := NewMDNSDiscoveryService(5 * time.Second).getIPv4Interface()
|
|
if auto == nil {
|
|
t.Skip("No suitable IPv4 interface available on this host")
|
|
}
|
|
|
|
explicit := NewMDNSDiscoveryServiceWithInterface(5*time.Second, auto.Name).getIPv4Interface()
|
|
if explicit == nil {
|
|
t.Fatalf("Expected explicit lookup of %q to succeed", auto.Name)
|
|
}
|
|
|
|
if explicit.Name != auto.Name {
|
|
t.Errorf("Expected explicit interface %q, got %q", auto.Name, explicit.Name)
|
|
}
|
|
|
|
// Sanity: the resolved interface really has an IPv4 we could bind to.
|
|
if !interfaceHasIPv4(explicit) {
|
|
t.Errorf("Resolved interface %q has no IPv4 address", explicit.Name)
|
|
}
|
|
}
|
|
|
|
func TestMDNSDiscoveryWithCancelledContext(t *testing.T) {
|
|
service := NewMDNSDiscoveryService(5 * time.Second)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
devices, err := service.DiscoverDevices(ctx)
|
|
|
|
// Should handle cancelled context gracefully
|
|
// devices should never be nil, even if cancelled
|
|
if devices == nil {
|
|
t.Error("Expected devices slice to be initialized, got nil")
|
|
}
|
|
|
|
// Error might or might not occur depending on timing and network conditions
|
|
// This is acceptable for testing - we just ensure no panic and proper slice initialization
|
|
_ = err
|
|
}
|