mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Mirrors the .md/.txt sweep across all tracked _test.go, testdata XML,
and .http integration files. Test files are self-contained (producer
+ assertion in the same file), so the matched-pair swap stays green
under `go test ./...`.
Mapping applied:
192.168.178.[0-9]+ → 192.0.2.[same]
192.168.1.[0-9]+ → 192.0.2.[same]
Sound Machinechen → Living Room SoundTouch
A Sound Machine → Kitchen SoundTouch
A81B6A536A98 + case/separator variants → AABBCCDDEEFF (etc.)
A81B6A849D99 → AABBCCDDEE01
A81B6A849D88 → AABBCCDDEE03
A81B6A536A09 → AABBCCDDEE04
884AEAEEBD27 → AABBCCDDEE02
3230304 → 1000001
9569497 → 1000002
Two semantic fixes alongside the bulk swap:
- pkg/service/zeroconf/zeroconf_test.go: the "private 192" and
"strips query" cases pin acceptance of RFC-1918 192.168/16. They
must use a real 192.168 value; doc-range IPs would (correctly) be
rejected by validateZcBaseURL. Switched to 192.168.10.10 — generic
enough not to match any home LAN default, real enough for the
validator. Added a comment explaining why this single test still
carries a 192.168 literal.
- pkg/service/setup/setup_test.go: TestTestDNSRedirection mocks the
device's `od -An -tu1` byte output, which is space-separated
octets ("192 168 1 100"). My sed only matched the dot-separated
form, so the mock was returning the old IP while the test
assertions had moved to the doc range. Updated to " 192 0 2 100".
go build ./... clean. go test ./... clean (only TestDocsConsistency
remains failing, which is a pre-existing/untracked-file issue).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
372 lines
7.9 KiB
Go
372 lines
7.9 KiB
Go
package telnet
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// scriptedServer is a minimal mock of the device's port-17000 shell. It
|
|
// returns the supplied banner on connect, then for each line read it emits
|
|
// the corresponding entry from responses (or "Command not found" if the line
|
|
// is not in the map).
|
|
type scriptedServer struct {
|
|
t *testing.T
|
|
listener net.Listener
|
|
banner string
|
|
responses map[string]string
|
|
// hangAfter, if non-empty, names a command after which the server stops
|
|
// responding (to exercise the read-timeout path).
|
|
hangAfter string
|
|
// closeAfter, if non-empty, names a command after which the server closes
|
|
// the connection mid-stream.
|
|
closeAfter string
|
|
|
|
stop chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newScriptedServer(t *testing.T, banner string, responses map[string]string) *scriptedServer {
|
|
t.Helper()
|
|
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
|
|
s := &scriptedServer{
|
|
t: t,
|
|
listener: l,
|
|
banner: banner,
|
|
responses: responses,
|
|
stop: make(chan struct{}),
|
|
}
|
|
|
|
s.wg.Add(1)
|
|
|
|
go s.serve()
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *scriptedServer) addr() string {
|
|
return s.listener.Addr().String()
|
|
}
|
|
|
|
func (s *scriptedServer) hostPort() (string, int) {
|
|
host, portStr, err := net.SplitHostPort(s.addr())
|
|
if err != nil {
|
|
s.t.Fatalf("split host/port: %v", err)
|
|
}
|
|
|
|
port := 0
|
|
|
|
if _, err := parseInt(portStr, &port); err != nil {
|
|
s.t.Fatalf("parse port %q: %v", portStr, err)
|
|
}
|
|
|
|
return host, port
|
|
}
|
|
|
|
func (s *scriptedServer) close() {
|
|
close(s.stop)
|
|
_ = s.listener.Close()
|
|
s.wg.Wait()
|
|
}
|
|
|
|
func (s *scriptedServer) serve() {
|
|
defer s.wg.Done()
|
|
|
|
conn, err := s.listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
if s.banner != "" {
|
|
_, _ = conn.Write([]byte(s.banner))
|
|
}
|
|
|
|
r := bufio.NewReader(conn)
|
|
|
|
for {
|
|
line, err := r.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cmd := strings.TrimRight(line, "\r\n")
|
|
if cmd == "" {
|
|
continue
|
|
}
|
|
|
|
if cmd == s.closeAfter {
|
|
return
|
|
}
|
|
|
|
resp, ok := s.responses[cmd]
|
|
if !ok {
|
|
resp = "Command not found\n"
|
|
}
|
|
|
|
_, _ = conn.Write([]byte(resp))
|
|
|
|
if cmd == s.hangAfter {
|
|
// Block until the server is closed; the client's read deadline
|
|
// must fire before then.
|
|
<-s.stop
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseInt is a tiny strconv.Atoi wrapper so we don't drag strconv into this file.
|
|
func parseInt(s string, out *int) (int, error) {
|
|
n := 0
|
|
|
|
for _, ch := range s {
|
|
if ch < '0' || ch > '9' {
|
|
return 0, errors.New("not a number")
|
|
}
|
|
|
|
n = n*10 + int(ch-'0')
|
|
}
|
|
|
|
*out = n
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func newClientFor(t *testing.T, s *scriptedServer) *Client {
|
|
t.Helper()
|
|
|
|
host, port := s.hostPort()
|
|
|
|
c := NewClient(host)
|
|
c.Port = port
|
|
// Tighten the timeouts so tests fail fast if the implementation regresses.
|
|
c.DialTimeout = 500 * time.Millisecond
|
|
c.ReadTimeout = 1500 * time.Millisecond
|
|
c.WriteTimeout = 500 * time.Millisecond
|
|
|
|
return c
|
|
}
|
|
|
|
func TestNewClient_Defaults(t *testing.T) {
|
|
c := NewClient("192.0.2.10")
|
|
if c.Host != "192.0.2.10" {
|
|
t.Errorf("Host = %q, want 192.0.2.10", c.Host)
|
|
}
|
|
|
|
if c.Port != DefaultPort {
|
|
t.Errorf("Port = %d, want %d", c.Port, DefaultPort)
|
|
}
|
|
|
|
if c.DialTimeout != DefaultDialTimeout {
|
|
t.Errorf("DialTimeout = %v, want %v", c.DialTimeout, DefaultDialTimeout)
|
|
}
|
|
}
|
|
|
|
func TestDial_Failure(t *testing.T) {
|
|
// A reserved-for-test address that nothing should be listening on.
|
|
c := NewClient("127.0.0.1")
|
|
c.Port = 1 // privileged port, will not connect from a test
|
|
c.DialTimeout = 200 * time.Millisecond
|
|
|
|
if err := c.Dial(); err == nil {
|
|
t.Error("expected dial failure, got nil")
|
|
}
|
|
}
|
|
|
|
func TestProbe_ReturnsBanner(t *testing.T) {
|
|
s := newScriptedServer(t, "BoseShell v1\n-> ", nil)
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
got, err := c.Probe()
|
|
if err != nil {
|
|
t.Fatalf("Probe: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(got, "BoseShell v1") {
|
|
t.Errorf("Probe = %q, want to contain banner", got)
|
|
}
|
|
}
|
|
|
|
func TestProbe_NoBannerIsOK(t *testing.T) {
|
|
s := newScriptedServer(t, "", nil)
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
got, err := c.Probe()
|
|
if err != nil {
|
|
t.Fatalf("Probe: %v", err)
|
|
}
|
|
|
|
if got != "" {
|
|
t.Errorf("Probe = %q, want empty when no banner is sent", got)
|
|
}
|
|
}
|
|
|
|
func TestSendCommand_HappyPath(t *testing.T) {
|
|
s := newScriptedServer(t, "", map[string]string{
|
|
"sys configuration bmxRegistryUrl http://example:8000/bmx/registry/v1/services": "OK\n",
|
|
"sys configuration margeServerUrl http://example:8000": "OK\n",
|
|
"getpdo CurrentSystemConfiguration": "margeServerUrl=http://example:8000\nbmxRegistryUrl=http://example:8000/bmx/registry/v1/services\n",
|
|
})
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
resp, err := c.SendCommand("sys configuration margeServerUrl http://example:8000")
|
|
if err != nil {
|
|
t.Fatalf("SendCommand: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(resp, "OK") {
|
|
t.Errorf("response = %q, want to contain OK", resp)
|
|
}
|
|
|
|
resp, err = c.SendCommand("getpdo CurrentSystemConfiguration")
|
|
if err != nil {
|
|
t.Fatalf("SendCommand getpdo: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(resp, "margeServerUrl=http://example:8000") {
|
|
t.Errorf("getpdo response = %q, want to echo configured url", resp)
|
|
}
|
|
}
|
|
|
|
func TestSendCommand_CommandNotFound(t *testing.T) {
|
|
s := newScriptedServer(t, "", map[string]string{})
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
resp, err := c.SendCommand("definitely not a real command")
|
|
if err != nil {
|
|
t.Fatalf("SendCommand: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(resp, "Command not found") {
|
|
t.Errorf("response = %q, want to contain 'Command not found'", resp)
|
|
}
|
|
}
|
|
|
|
func TestSendCommand_DeadlineFiresWhenDeviceHangs(t *testing.T) {
|
|
s := newScriptedServer(t, "", map[string]string{
|
|
"first": "OK\n",
|
|
"second": "",
|
|
})
|
|
s.hangAfter = "second"
|
|
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
c.ReadTimeout = 600 * time.Millisecond
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
if _, err := c.SendCommand("first"); err != nil {
|
|
t.Fatalf("first SendCommand: %v", err)
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
_, err := c.SendCommand("second")
|
|
if err == nil {
|
|
t.Fatal("expected timeout error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "timed out") {
|
|
t.Errorf("err = %v, want timed-out wording", err)
|
|
}
|
|
|
|
// The error must arrive within roughly the ReadTimeout, not after several
|
|
// times that — guards against an accidental infinite read loop.
|
|
if elapsed := time.Since(start); elapsed > 2*time.Second {
|
|
t.Errorf("SendCommand returned after %v, want under 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestSendCommand_ConnectionClosedMidStream(t *testing.T) {
|
|
s := newScriptedServer(t, "", map[string]string{})
|
|
s.closeAfter = "trigger close"
|
|
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
defer func() { _ = c.Close() }()
|
|
|
|
_, err := c.SendCommand("trigger close")
|
|
if err == nil {
|
|
t.Fatal("expected error after server closes mid-stream, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSendCommand_FailsWithoutDial(t *testing.T) {
|
|
c := NewClient("127.0.0.1")
|
|
if _, err := c.SendCommand("anything"); err == nil {
|
|
t.Error("SendCommand without Dial should fail, got nil")
|
|
}
|
|
}
|
|
|
|
func TestClose_IsIdempotent(t *testing.T) {
|
|
s := newScriptedServer(t, "", nil)
|
|
defer s.close()
|
|
|
|
c := newClientFor(t, s)
|
|
|
|
if err := c.Dial(); err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("first Close: %v", err)
|
|
}
|
|
|
|
if err := c.Close(); err != nil {
|
|
t.Errorf("second Close: %v", err)
|
|
}
|
|
}
|