mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 17:16:16 +00:00
Confirmed on real hardware (192.168.178.28): RevertMigration's full call graph (revertXMLConfig/revertHosts/revertResolvConf/revertAftertouchHook/ removeRcLocalHooks/revertCACert) makes 17 separate client.Run() calls, and pkg/ssh.Client.Run/UploadContent each dialed a brand-new SSH connection per call with no reuse. Hitting a resource-constrained speaker with 17 rapid reconnects overwhelmed it -- confirmed via a follow-up plain SSH command timing out at the TCP level, and the speaker going visibly unresponsive. Gives pkg/ssh.Client an opt-in persistent connection: Connect() dials once and caches it, Close() releases it, and a shared dial() helper makes Run/UploadContent reuse the cached connection when one's open, falling back to today's per-call dial otherwise. RevertMigration now calls Connect() once and defer Close(), collapsing 17 connections into 1. The other ~21 m.NewSSH() call sites in pkg/service/setup never call Connect, so their behavior is completely unchanged -- this only touches the one function that was actually causing real-world problems. SSHClient interface gained Connect()/Close(); both test mocks (pkg/service/setup/setup_test.go, pkg/service/handlers/handlers_setup_test.go) got no-op stubs. Added TestClose_NoOpWithoutConnect and TestConnect_DialFailureLeavesConnNil in pkg/ssh/ssh_test.go -- these don't prove connection reuse against a real server (Client.Run hardcodes :22, no configurable port for a test listener), so that specific behavior is verified by code review (a single `if c.conn != nil` branch) plus the real-hardware confirmation above, not an automated integration test. Also fixes the web UI's "Revert to Defaults" button, which calls the same RevertMigration code path.
77 lines
1.7 KiB
Go
77 lines
1.7 KiB
Go
package ssh
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewClient(t *testing.T) {
|
|
host := "192.0.2.10"
|
|
|
|
client := NewClient(host)
|
|
if client.Host != host {
|
|
t.Errorf("Expected host %s, got %s", host, client.Host)
|
|
}
|
|
|
|
if client.User != "root" {
|
|
t.Errorf("Expected user root, got %s", client.User)
|
|
}
|
|
}
|
|
|
|
func TestGetConfig(t *testing.T) {
|
|
client := NewClient("localhost")
|
|
|
|
config := client.getConfig()
|
|
if config.User != "root" {
|
|
t.Errorf("Expected config user root, got %s", config.User)
|
|
}
|
|
|
|
if len(config.Auth) == 0 {
|
|
t.Error("Expected at least one auth method")
|
|
}
|
|
}
|
|
|
|
func TestRun_DialFailure(t *testing.T) {
|
|
// Use an invalid port/host to trigger dial failure
|
|
client := NewClient("127.0.0.1:0")
|
|
|
|
_, err := client.Run("ls")
|
|
if err == nil {
|
|
t.Error("Expected dial failure, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "failed to dial") {
|
|
t.Errorf("Expected 'failed to dial' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClose_NoOpWithoutConnect(t *testing.T) {
|
|
client := NewClient("127.0.0.1")
|
|
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("Close on a never-connected client should be a no-op, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnect_DialFailureLeavesConnNil(t *testing.T) {
|
|
client := NewClient("127.0.0.1:0")
|
|
|
|
err := client.Connect()
|
|
if err == nil {
|
|
t.Fatal("Expected dial failure, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "failed to dial") {
|
|
t.Errorf("Expected 'failed to dial' error, got: %v", err)
|
|
}
|
|
|
|
if client.conn != nil {
|
|
t.Error("Connect should leave conn nil after a dial failure, so Run/UploadContent still fall back to their own one-off dial")
|
|
}
|
|
|
|
// Close after a failed Connect should still be a harmless no-op.
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("Close after a failed Connect should be a no-op, got: %v", err)
|
|
}
|
|
}
|