mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat: implement bass control (GET/POST /bass)
- Add complete bass control functionality via GET/POST /bass endpoints
- Implement GetBass() for current bass level retrieval
- Add SetBass() with range validation (-9 to +9)
- Include IncreaseBass() and DecreaseBass() with safety limits
- Add SetBassSafe() with automatic value clamping
- Create comprehensive bass models with validation and helpers
- Add CLI flags: -bass, -set-bass, -inc-bass, -dec-bass
- Implement safety features with range validation and clamping
- Create comprehensive test suite (30+ test cases) with mock servers
- Add integration tests with real device validation (SoundTouch 10/20)
- Update documentation with complete BASS-CONTROLS.md guide
- Update API endpoints status (GET/POST /bass: ✅ Implemented)
- Update project status (55% overall completion, 80% control endpoints)
- Real device testing with bass adjustment and validation
- Error handling for invalid ranges and API responses
- XML request/response format validation and compliance
- Human-readable bass level descriptions and categorization
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/user_account/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
// Integration tests for bass control functionality
|
||||
// These tests require a real SoundTouch device for validation
|
||||
// Set SOUNDTOUCH_TEST_HOST environment variable to run these tests
|
||||
|
||||
func TestClient_Bass_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
t.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration tests")
|
||||
}
|
||||
|
||||
// Parse host:port if provided
|
||||
finalHost, finalPort := parseBassHostPort(host, 8090)
|
||||
|
||||
config := ClientConfig{
|
||||
Host: finalHost,
|
||||
Port: finalPort,
|
||||
Timeout: 15 * time.Second,
|
||||
UserAgent: "Bose-SoundTouch-Go-Bass-Integration-Test/1.0",
|
||||
}
|
||||
|
||||
client := NewClient(config)
|
||||
|
||||
t.Run("GetBass", func(t *testing.T) {
|
||||
t.Logf("Testing GetBass on %s:%d", finalHost, finalPort)
|
||||
|
||||
bass, err := client.GetBass()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get bass: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ Current bass level: %d (%s)", bass.GetLevel(), bass.String())
|
||||
t.Logf("✓ Bass category: %s", models.GetBassLevelCategory(bass.GetLevel()))
|
||||
t.Logf("✓ Target vs Actual: %d vs %d", bass.TargetBass, bass.ActualBass)
|
||||
|
||||
// Validate bass level is within expected range
|
||||
if bass.GetLevel() < -9 || bass.GetLevel() > 9 {
|
||||
t.Errorf("Bass level %d is outside valid range [-9, 9]", bass.GetLevel())
|
||||
}
|
||||
|
||||
// Device ID should be present
|
||||
if bass.DeviceID == "" {
|
||||
t.Error("Device ID should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetBass", func(t *testing.T) {
|
||||
t.Logf("Testing SetBass on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Get original bass level first
|
||||
originalBass, err := client.GetBass()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get original bass level: %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("Original bass level: %d", originalBass.GetLevel())
|
||||
|
||||
// Try setting to 0 (neutral)
|
||||
err = client.SetBass(0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set bass to 0: %v", err)
|
||||
return
|
||||
}
|
||||
t.Log("✓ SetBass(0) completed successfully")
|
||||
|
||||
// Give device time to process
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Verify the change (note: some devices may override this)
|
||||
newBass, err := client.GetBass()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get bass after setting: %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("Bass after setting to 0: %d", newBass.GetLevel())
|
||||
|
||||
// Restore original bass level
|
||||
err = client.SetBass(originalBass.GetLevel())
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to restore original bass level %d: %v", originalBass.GetLevel(), err)
|
||||
} else {
|
||||
t.Logf("✓ Restored original bass level: %d", originalBass.GetLevel())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetBassWithValidation", func(t *testing.T) {
|
||||
t.Logf("Testing bass validation on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Test valid range boundaries
|
||||
validLevels := []int{-9, -5, 0, 5, 9}
|
||||
for _, level := range validLevels {
|
||||
err := client.SetBass(level)
|
||||
if err != nil {
|
||||
t.Errorf("SetBass(%d) should succeed, got error: %v", level, err)
|
||||
} else {
|
||||
t.Logf("✓ SetBass(%d) accepted", level)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond) // Brief pause between commands
|
||||
}
|
||||
|
||||
// Test invalid levels (should fail validation before hitting device)
|
||||
invalidLevels := []int{-10, -100, 10, 100}
|
||||
for _, level := range invalidLevels {
|
||||
err := client.SetBass(level)
|
||||
if err == nil {
|
||||
t.Errorf("SetBass(%d) should fail validation, got nil error", level)
|
||||
} else {
|
||||
t.Logf("✓ SetBass(%d) correctly rejected: %v", level, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetBassSafe", func(t *testing.T) {
|
||||
t.Logf("Testing SetBassSafe (clamping) on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Test clamping behavior
|
||||
tests := []struct {
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{input: 15, expected: 9}, // Clamp high
|
||||
{input: -15, expected: -9}, // Clamp low
|
||||
{input: 5, expected: 5}, // No clamp needed
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := client.SetBassSafe(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("SetBassSafe(%d) failed: %v", test.input, err)
|
||||
} else {
|
||||
t.Logf("✓ SetBassSafe(%d) completed (should clamp to %d)", test.input, test.expected)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Bass_IncrementDecrement_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
t.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration tests")
|
||||
}
|
||||
|
||||
// Parse host:port if provided
|
||||
finalHost, finalPort := parseBassHostPort(host, 8090)
|
||||
|
||||
config := ClientConfig{
|
||||
Host: finalHost,
|
||||
Port: finalPort,
|
||||
Timeout: 15 * time.Second,
|
||||
UserAgent: "Bose-SoundTouch-Go-Bass-Integration-Test/1.0",
|
||||
}
|
||||
|
||||
client := NewClient(config)
|
||||
|
||||
// Get and store original bass level
|
||||
originalBass, err := client.GetBass()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get original bass level: %v", err)
|
||||
}
|
||||
t.Logf("Original bass level: %d", originalBass.GetLevel())
|
||||
|
||||
// Ensure we restore original level at the end
|
||||
defer func() {
|
||||
err := client.SetBass(originalBass.GetLevel())
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to restore original bass level: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("IncreaseBass", func(t *testing.T) {
|
||||
t.Logf("Testing IncreaseBass on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Set to known starting point
|
||||
err := client.SetBass(0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set bass to starting point: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// Test increasing by 1
|
||||
bass, err := client.IncreaseBass(1)
|
||||
if err != nil {
|
||||
t.Errorf("IncreaseBass(1) failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ IncreaseBass(1) completed, result: %d (%s)", bass.GetLevel(), bass.String())
|
||||
|
||||
// Test that result is returned correctly
|
||||
if bass == nil {
|
||||
t.Error("IncreaseBass should return non-nil bass result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DecreaseBass", func(t *testing.T) {
|
||||
t.Logf("Testing DecreaseBass on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Set to known starting point
|
||||
err := client.SetBass(0)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set bass to starting point: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// Test decreasing by 1
|
||||
bass, err := client.DecreaseBass(1)
|
||||
if err != nil {
|
||||
t.Errorf("DecreaseBass(1) failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ DecreaseBass(1) completed, result: %d (%s)", bass.GetLevel(), bass.String())
|
||||
|
||||
// Test that result is returned correctly
|
||||
if bass == nil {
|
||||
t.Error("DecreaseBass should return non-nil bass result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BassClampingBehavior", func(t *testing.T) {
|
||||
t.Logf("Testing bass clamping behavior on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Test increase near maximum
|
||||
err := client.SetBass(8)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set bass to 8: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
bass, err := client.IncreaseBass(3) // Should clamp to 9
|
||||
if err != nil {
|
||||
t.Errorf("IncreaseBass(3) from 8 failed: %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("✓ IncreaseBass(3) from 8 result: %d (should be clamped)", bass.GetLevel())
|
||||
|
||||
// Test decrease near minimum
|
||||
err = client.SetBass(-8)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set bass to -8: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
bass, err = client.DecreaseBass(3) // Should clamp to -9
|
||||
if err != nil {
|
||||
t.Errorf("DecreaseBass(3) from -8 failed: %v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("✓ DecreaseBass(3) from -8 result: %d (should be clamped)", bass.GetLevel())
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Bass_ErrorHandling_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
t.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration tests")
|
||||
}
|
||||
|
||||
// Parse host:port if provided
|
||||
finalHost, finalPort := parseBassHostPort(host, 8090)
|
||||
|
||||
config := ClientConfig{
|
||||
Host: finalHost,
|
||||
Port: finalPort,
|
||||
Timeout: 15 * time.Second,
|
||||
UserAgent: "Bose-SoundTouch-Go-Bass-Integration-Test/1.0",
|
||||
}
|
||||
|
||||
client := NewClient(config)
|
||||
|
||||
t.Run("ValidationErrors", func(t *testing.T) {
|
||||
t.Logf("Testing bass validation errors on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Test out-of-range values
|
||||
invalidLevels := []int{-10, -100, 10, 50, 100}
|
||||
for _, level := range invalidLevels {
|
||||
err := client.SetBass(level)
|
||||
if err == nil {
|
||||
t.Errorf("SetBass(%d) should have failed validation", level)
|
||||
} else {
|
||||
t.Logf("✓ SetBass(%d) correctly failed: %v", level, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IncrementDecrementErrors", func(t *testing.T) {
|
||||
t.Logf("Testing increment/decrement error conditions on %s:%d", finalHost, finalPort)
|
||||
|
||||
// Test with very large increments (should be clamped, not error)
|
||||
_, err := client.IncreaseBass(100)
|
||||
if err != nil {
|
||||
t.Errorf("IncreaseBass(100) should clamp, not error: %v", err)
|
||||
} else {
|
||||
t.Log("✓ IncreaseBass(100) handled with clamping")
|
||||
}
|
||||
|
||||
_, err = client.DecreaseBass(100)
|
||||
if err != nil {
|
||||
t.Errorf("DecreaseBass(100) should clamp, not error: %v", err)
|
||||
} else {
|
||||
t.Log("✓ DecreaseBass(100) handled with clamping")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark bass control performance
|
||||
func BenchmarkClient_Bass_Integration(b *testing.B) {
|
||||
if testing.Short() {
|
||||
b.Skip("Skipping integration benchmarks in short mode")
|
||||
}
|
||||
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
b.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration benchmarks")
|
||||
}
|
||||
|
||||
// Parse host:port if provided
|
||||
finalHost, finalPort := parseBassHostPort(host, 8090)
|
||||
|
||||
config := ClientConfig{
|
||||
Host: finalHost,
|
||||
Port: finalPort,
|
||||
Timeout: 15 * time.Second,
|
||||
UserAgent: "Bose-SoundTouch-Go-Bass-Benchmark/1.0",
|
||||
}
|
||||
|
||||
client := NewClient(config)
|
||||
|
||||
// Get original bass level for restoration
|
||||
originalBass, err := client.GetBass()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to get original bass: %v", err)
|
||||
}
|
||||
|
||||
// Restore original bass at the end
|
||||
defer func() {
|
||||
client.SetBass(originalBass.GetLevel())
|
||||
}()
|
||||
|
||||
b.Run("GetBass", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.GetBass()
|
||||
if err != nil {
|
||||
b.Fatalf("GetBass failed: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetBass", func(b *testing.B) {
|
||||
bassLevels := []int{-3, 0, 3, -1, 1} // Cycle through different levels
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
level := bassLevels[i%len(bassLevels)]
|
||||
err := client.SetBass(level)
|
||||
if err != nil {
|
||||
b.Fatalf("SetBass(%d) failed: %v", level, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IncreaseBass", func(b *testing.B) {
|
||||
// Set to a safe starting point
|
||||
client.SetBass(-3)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Keep increments small to avoid hitting limits
|
||||
_, err := client.IncreaseBass(1)
|
||||
if err != nil {
|
||||
b.Fatalf("IncreaseBass failed: %v", err)
|
||||
}
|
||||
// Reset to safe level periodically
|
||||
if i%3 == 0 {
|
||||
client.SetBass(-3)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// parseBassHostPort is a helper function for integration tests
|
||||
// This is a simple version for test use
|
||||
func parseBassHostPort(hostPort string, defaultPort int) (string, int) {
|
||||
if !containsSubstring(hostPort, ":") {
|
||||
return hostPort, defaultPort
|
||||
}
|
||||
|
||||
// Simple parsing - in real use, we'd use net.SplitHostPort
|
||||
parts := make([]string, 0, 2)
|
||||
current := ""
|
||||
for _, char := range hostPort {
|
||||
if char == ':' {
|
||||
parts = append(parts, current)
|
||||
current = ""
|
||||
} else {
|
||||
current += string(char)
|
||||
}
|
||||
}
|
||||
if current != "" {
|
||||
parts = append(parts, current)
|
||||
}
|
||||
|
||||
if len(parts) == 2 {
|
||||
// Try to parse port
|
||||
port := defaultPort
|
||||
portStr := parts[1]
|
||||
portInt := 0
|
||||
for _, char := range portStr {
|
||||
if char >= '0' && char <= '9' {
|
||||
portInt = portInt*10 + int(char-'0')
|
||||
} else {
|
||||
portInt = -1
|
||||
break
|
||||
}
|
||||
}
|
||||
if portInt > 0 && portInt <= 65535 {
|
||||
port = portInt
|
||||
}
|
||||
return parts[0], port
|
||||
}
|
||||
|
||||
return hostPort, defaultPort
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/user_account/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func TestClient_GetBass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serverResponse string
|
||||
wantError bool
|
||||
wantTargetBass int
|
||||
wantActualBass int
|
||||
wantDeviceID string
|
||||
}{
|
||||
{
|
||||
name: "Valid bass response",
|
||||
serverResponse: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>3</targetbass>
|
||||
<actualbass>3</actualbass>
|
||||
</bass>`,
|
||||
wantError: false,
|
||||
wantTargetBass: 3,
|
||||
wantActualBass: 3,
|
||||
wantDeviceID: "1234567890AB",
|
||||
},
|
||||
{
|
||||
name: "Negative bass response",
|
||||
serverResponse: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>-5</targetbass>
|
||||
<actualbass>-5</actualbass>
|
||||
</bass>`,
|
||||
wantError: false,
|
||||
wantTargetBass: -5,
|
||||
wantActualBass: -5,
|
||||
wantDeviceID: "1234567890AB",
|
||||
},
|
||||
{
|
||||
name: "Zero bass response",
|
||||
serverResponse: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>0</targetbass>
|
||||
<actualbass>0</actualbass>
|
||||
</bass>`,
|
||||
wantError: false,
|
||||
wantTargetBass: 0,
|
||||
wantActualBass: 0,
|
||||
wantDeviceID: "1234567890AB",
|
||||
},
|
||||
{
|
||||
name: "Bass adjustment in progress",
|
||||
serverResponse: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>6</targetbass>
|
||||
<actualbass>4</actualbass>
|
||||
</bass>`,
|
||||
wantError: false,
|
||||
wantTargetBass: 6,
|
||||
wantActualBass: 4,
|
||||
wantDeviceID: "1234567890AB",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
return
|
||||
}
|
||||
if r.URL.Path != "/bass" {
|
||||
t.Errorf("Expected path /bass, got %s", r.URL.Path)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(tt.serverResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:], // Remove "http://"
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
bass, err := client.GetBass()
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if bass.TargetBass != tt.wantTargetBass {
|
||||
t.Errorf("Expected target bass %d, got %d", tt.wantTargetBass, bass.TargetBass)
|
||||
}
|
||||
if bass.ActualBass != tt.wantActualBass {
|
||||
t.Errorf("Expected actual bass %d, got %d", tt.wantActualBass, bass.ActualBass)
|
||||
}
|
||||
if bass.DeviceID != tt.wantDeviceID {
|
||||
t.Errorf("Expected device ID %s, got %s", tt.wantDeviceID, bass.DeviceID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SetBass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level int
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid bass level 0",
|
||||
level: 0,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid bass level +9",
|
||||
level: 9,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid bass level -9",
|
||||
level: -9,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid bass level +3",
|
||||
level: 3,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid bass level -3",
|
||||
level: -3,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid bass level +10",
|
||||
level: 10,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid bass level -10",
|
||||
level: -10,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid bass level +100",
|
||||
level: 100,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.wantError {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("Expected POST request, got %s", r.Method)
|
||||
return
|
||||
}
|
||||
if r.URL.Path != "/bass" {
|
||||
t.Errorf("Expected path /bass, got %s", r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Content-Type
|
||||
if contentType := r.Header.Get("Content-Type"); contentType != "application/xml" {
|
||||
t.Errorf("Expected Content-Type application/xml, got %s", contentType)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate request body
|
||||
var bassReq models.BassRequest
|
||||
err := xml.NewDecoder(r.Body).Decode(&bassReq)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode request XML: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if bassReq.Level != tt.level {
|
||||
t.Errorf("Expected bass level %d, got %d", tt.level, bassReq.Level)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
err := client.SetBass(tt.level)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Test validation without server
|
||||
config := ClientConfig{
|
||||
Host: "localhost",
|
||||
Port: 8090,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
|
||||
err := client.SetBass(tt.level)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for invalid bass level %d, got nil", tt.level)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SetBassSafe(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level int
|
||||
expectedLevel int
|
||||
}{
|
||||
{
|
||||
name: "Valid level unchanged",
|
||||
level: 3,
|
||||
expectedLevel: 3,
|
||||
},
|
||||
{
|
||||
name: "Too high clamped",
|
||||
level: 15,
|
||||
expectedLevel: 9,
|
||||
},
|
||||
{
|
||||
name: "Too low clamped",
|
||||
level: -15,
|
||||
expectedLevel: -9,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse request body to verify clamped level
|
||||
var bassReq models.BassRequest
|
||||
err := xml.NewDecoder(r.Body).Decode(&bassReq)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode request XML: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if bassReq.Level != tt.expectedLevel {
|
||||
t.Errorf("Expected clamped bass level %d, got %d", tt.expectedLevel, bassReq.Level)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
err := client.SetBassSafe(tt.level)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_IncreaseBass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentBass int
|
||||
amount int
|
||||
expectedNewBass int
|
||||
}{
|
||||
{
|
||||
name: "Normal increase",
|
||||
currentBass: 0,
|
||||
amount: 3,
|
||||
expectedNewBass: 3,
|
||||
},
|
||||
{
|
||||
name: "Increase with clamping",
|
||||
currentBass: 8,
|
||||
amount: 3,
|
||||
expectedNewBass: 9,
|
||||
},
|
||||
{
|
||||
name: "Increase from negative",
|
||||
currentBass: -3,
|
||||
amount: 2,
|
||||
expectedNewBass: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
getCallCount := 0
|
||||
postCallCount := 0
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
|
||||
if r.Method == "GET" && r.URL.Path == "/bass" {
|
||||
getCallCount++
|
||||
// Return current bass level
|
||||
response := `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>` + string(rune(tt.currentBass+48)) + `</targetbass>
|
||||
<actualbass>` + string(rune(tt.currentBass+48)) + `</actualbass>
|
||||
</bass>`
|
||||
if getCallCount == 1 {
|
||||
// First call - return current bass
|
||||
if tt.currentBass >= 0 && tt.currentBass <= 9 {
|
||||
response = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>` + string(rune(tt.currentBass+'0')) + `</targetbass>
|
||||
<actualbass>` + string(rune(tt.currentBass+'0')) + `</actualbass>
|
||||
</bass>`
|
||||
} else {
|
||||
// Handle negative numbers
|
||||
response = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<bass deviceID="1234567890AB">
|
||||
<targetbass>` + string(rune(-tt.currentBass+'0')) + `</targetbass>
|
||||
<actualbass>` + string(rune(-tt.currentBass+'0')) + `</actualbass>
|
||||
</bass>`
|
||||
}
|
||||
// For simplicity in testing, let's use a different approach
|
||||
if tt.currentBass == 0 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>0</targetbass><actualbass>0</actualbass></bass>`
|
||||
} else if tt.currentBass == 8 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>8</targetbass><actualbass>8</actualbass></bass>`
|
||||
} else if tt.currentBass == -3 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>-3</targetbass><actualbass>-3</actualbass></bass>`
|
||||
}
|
||||
} else {
|
||||
// Second call - return new bass level
|
||||
if tt.expectedNewBass == 3 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>3</targetbass><actualbass>3</actualbass></bass>`
|
||||
} else if tt.expectedNewBass == 9 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>9</targetbass><actualbass>9</actualbass></bass>`
|
||||
} else if tt.expectedNewBass == -1 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>-1</targetbass><actualbass>-1</actualbass></bass>`
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(response))
|
||||
} else if r.Method == "POST" && r.URL.Path == "/bass" {
|
||||
postCallCount++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
bass, err := client.IncreaseBass(tt.amount)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if bass.GetLevel() != tt.expectedNewBass {
|
||||
t.Errorf("Expected new bass level %d, got %d", tt.expectedNewBass, bass.GetLevel())
|
||||
}
|
||||
|
||||
if getCallCount != 2 {
|
||||
t.Errorf("Expected 2 GET calls, got %d", getCallCount)
|
||||
}
|
||||
if postCallCount != 1 {
|
||||
t.Errorf("Expected 1 POST call, got %d", postCallCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_DecreaseBass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentBass int
|
||||
amount int
|
||||
expectedNewBass int
|
||||
}{
|
||||
{
|
||||
name: "Normal decrease",
|
||||
currentBass: 3,
|
||||
amount: 2,
|
||||
expectedNewBass: 1,
|
||||
},
|
||||
{
|
||||
name: "Decrease with clamping",
|
||||
currentBass: -7,
|
||||
amount: 5,
|
||||
expectedNewBass: -9,
|
||||
},
|
||||
{
|
||||
name: "Decrease to negative",
|
||||
currentBass: 2,
|
||||
amount: 4,
|
||||
expectedNewBass: -2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
getCallCount := 0
|
||||
postCallCount := 0
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
|
||||
if r.Method == "GET" && r.URL.Path == "/bass" {
|
||||
getCallCount++
|
||||
var response string
|
||||
if getCallCount == 1 {
|
||||
// First call - return current bass
|
||||
if tt.currentBass == 3 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>3</targetbass><actualbass>3</actualbass></bass>`
|
||||
} else if tt.currentBass == -7 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>-7</targetbass><actualbass>-7</actualbass></bass>`
|
||||
} else if tt.currentBass == 2 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>2</targetbass><actualbass>2</actualbass></bass>`
|
||||
}
|
||||
} else {
|
||||
// Second call - return new bass level
|
||||
if tt.expectedNewBass == 1 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>1</targetbass><actualbass>1</actualbass></bass>`
|
||||
} else if tt.expectedNewBass == -9 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>-9</targetbass><actualbass>-9</actualbass></bass>`
|
||||
} else if tt.expectedNewBass == -2 {
|
||||
response = `<bass deviceID="1234567890AB"><targetbass>-2</targetbass><actualbass>-2</actualbass></bass>`
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(response))
|
||||
} else if r.Method == "POST" && r.URL.Path == "/bass" {
|
||||
postCallCount++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
bass, err := client.DecreaseBass(tt.amount)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if bass.GetLevel() != tt.expectedNewBass {
|
||||
t.Errorf("Expected new bass level %d, got %d", tt.expectedNewBass, bass.GetLevel())
|
||||
}
|
||||
|
||||
if getCallCount != 2 {
|
||||
t.Errorf("Expected 2 GET calls, got %d", getCallCount)
|
||||
}
|
||||
if postCallCount != 1 {
|
||||
t.Errorf("Expected 1 POST call, got %d", postCallCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Bass_ErrorHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serverResponse func(w http.ResponseWriter, r *http.Request)
|
||||
method func(*Client) error
|
||||
wantError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "GetBass server returns 404",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Not Found"))
|
||||
},
|
||||
method: func(c *Client) error {
|
||||
_, err := c.GetBass()
|
||||
return err
|
||||
},
|
||||
wantError: true,
|
||||
errorContains: "failed to get bass",
|
||||
},
|
||||
{
|
||||
name: "SetBass server returns 500",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal Server Error"))
|
||||
},
|
||||
method: func(c *Client) error {
|
||||
return c.SetBass(3)
|
||||
},
|
||||
wantError: true,
|
||||
errorContains: "API request failed with status 500",
|
||||
},
|
||||
{
|
||||
name: "GetBass invalid XML response",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("invalid xml"))
|
||||
},
|
||||
method: func(c *Client) error {
|
||||
_, err := c.GetBass()
|
||||
return err
|
||||
},
|
||||
wantError: true,
|
||||
errorContains: "failed to get bass",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
err := tt.method(client)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else if !containsSubstring(err.Error(), tt.errorContains) {
|
||||
t.Errorf("Expected error containing '%s', got '%s'", tt.errorContains, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Bass_RequestFormat(t *testing.T) {
|
||||
// Test that the request XML format is correct
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Read and parse the raw request body
|
||||
var bassReq models.BassRequest
|
||||
err := xml.NewDecoder(r.Body).Decode(&bassReq)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode request XML: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate XML structure
|
||||
expectedLevel := 5
|
||||
if bassReq.Level != expectedLevel {
|
||||
t.Errorf("Expected bass level %d, got %d", expectedLevel, bassReq.Level)
|
||||
}
|
||||
|
||||
// Re-encode to verify XML format
|
||||
actualXML, err := xml.Marshal(bassReq)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to marshal BassRequest: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expectedXML := "<bass>5</bass>"
|
||||
if string(actualXML) != expectedXML {
|
||||
t.Errorf("Expected XML '%s', got '%s'", expectedXML, string(actualXML))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := ClientConfig{
|
||||
Host: server.URL[7:],
|
||||
Port: 80,
|
||||
Timeout: testTimeout,
|
||||
UserAgent: testUserAgent,
|
||||
}
|
||||
client := NewClient(config)
|
||||
client.baseURL = server.URL
|
||||
|
||||
err := client.SetBass(5)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -296,6 +296,70 @@ func (c *Client) DecreaseVolume(amount int) (*models.Volume, error) {
|
||||
return c.GetVolume()
|
||||
}
|
||||
|
||||
// GetBass retrieves the current bass level from the /bass endpoint
|
||||
func (c *Client) GetBass() (*models.Bass, error) {
|
||||
var bass models.Bass
|
||||
err := c.get("/bass", &bass)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get bass: %w", err)
|
||||
}
|
||||
return &bass, nil
|
||||
}
|
||||
|
||||
// SetBass sets the bass level using the /bass endpoint
|
||||
func (c *Client) SetBass(level int) error {
|
||||
if !models.ValidateBassLevel(level) {
|
||||
return fmt.Errorf("invalid bass level: %d (must be between %d and %d)", level, models.BassLevelMin, models.BassLevelMax)
|
||||
}
|
||||
|
||||
bassReq, err := models.NewBassRequest(level)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bass request: %w", err)
|
||||
}
|
||||
|
||||
return c.post("/bass", bassReq, nil)
|
||||
}
|
||||
|
||||
// SetBassSafe sets bass with validation and clamping
|
||||
func (c *Client) SetBassSafe(level int) error {
|
||||
clampedLevel := models.ClampBassLevel(level)
|
||||
return c.SetBass(clampedLevel)
|
||||
}
|
||||
|
||||
// IncreaseBass increases bass by the specified amount (with safety limits)
|
||||
func (c *Client) IncreaseBass(amount int) (*models.Bass, error) {
|
||||
currentBass, err := c.GetBass()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current bass: %w", err)
|
||||
}
|
||||
|
||||
newLevel := models.ClampBassLevel(currentBass.GetLevel() + amount)
|
||||
err = c.SetBass(newLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set bass: %w", err)
|
||||
}
|
||||
|
||||
// Return updated bass
|
||||
return c.GetBass()
|
||||
}
|
||||
|
||||
// DecreaseBass decreases bass by the specified amount (with safety limits)
|
||||
func (c *Client) DecreaseBass(amount int) (*models.Bass, error) {
|
||||
currentBass, err := c.GetBass()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current bass: %w", err)
|
||||
}
|
||||
|
||||
newLevel := models.ClampBassLevel(currentBass.GetLevel() - amount)
|
||||
err = c.SetBass(newLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set bass: %w", err)
|
||||
}
|
||||
|
||||
// Return updated bass
|
||||
return c.GetBass()
|
||||
}
|
||||
|
||||
// SelectSource selects an audio source using the /select endpoint
|
||||
func (c *Client) SelectSource(source string, sourceAccount string) error {
|
||||
// Validate source parameter
|
||||
|
||||
Reference in New Issue
Block a user