Files
Bose-SoundTouch/pkg/models/key_test.go
T
Tobias Gesellchen 7d73da7986 feat: implement POST /key endpoint for media controls with host:port parsing
Major Features:
• POST /key endpoint implementation with XML model and validation
• Comprehensive media control commands (play, pause, stop, volume, presets)
• Automatic host:port parsing in CLI for improved UX
• Production-ready with full test coverage

Key Control Implementation:
• Add Key model with XML marshaling and validation (pkg/models/key.go)
• Support all standard keys: PLAY, PAUSE, STOP, PREV_TRACK, NEXT_TRACK, VOLUME_UP/DOWN, PRESET_1-6
• Client methods: SendKey(), Play(), Pause(), Stop(), VolumeUp(), VolumeDown(), SelectPreset()
• CLI commands: -play, -pause, -stop, -next, -prev, -volume-up, -volume-down, -preset, -key
• Critical fix: Use 'Gabbo' as sender (only accepted value by SoundTouch API)

Host:Port Parsing Enhancement:
• Support -host 192.168.178.28:8090 format in addition to separate -host/-port flags
• Robust parsing with IPv4, IPv6, and hostname support
• Graceful fallback for invalid input
• Backward compatible with existing usage

Testing & Documentation:
• Comprehensive unit tests for key functionality and host:port parsing
• Integration tested with real SoundTouch 10 and SoundTouch 20 devices
• Complete documentation in docs/KEY-CONTROLS.md and docs/HOST-PORT-PARSING.md
• All tests pass, no diagnostics errors

Breaking Changes: None
Backward Compatibility: Fully maintained

Tested with:
• SoundTouch 10 (192.168.178.28:8090) 
• SoundTouch 20 (192.168.178.35:8090) 
2026-01-08 23:46:34 +01:00

287 lines
6.1 KiB
Go

package models
import (
"encoding/xml"
"testing"
)
func TestNewKey(t *testing.T) {
key := NewKey(KeyPlay)
if key.State != KeyStatePress {
t.Errorf("Expected state %q, got %q", KeyStatePress, key.State)
}
if key.Sender != "Gabbo" {
t.Errorf("Expected sender %q, got %q", "Gabbo", key.Sender)
}
if key.Value != KeyPlay {
t.Errorf("Expected value %q, got %q", KeyPlay, key.Value)
}
}
func TestNewKeyPress(t *testing.T) {
key := NewKeyPress(KeyPause)
if key.State != KeyStatePress {
t.Errorf("Expected state %q, got %q", KeyStatePress, key.State)
}
if key.Value != KeyPause {
t.Errorf("Expected value %q, got %q", KeyPause, key.Value)
}
}
func TestNewKeyRelease(t *testing.T) {
key := NewKeyRelease(KeyStop)
if key.State != KeyStateRelease {
t.Errorf("Expected state %q, got %q", KeyStateRelease, key.State)
}
if key.Sender != "Gabbo" {
t.Errorf("Expected sender %q, got %q", "Gabbo", key.Sender)
}
if key.Value != KeyStop {
t.Errorf("Expected value %q, got %q", KeyStop, key.Value)
}
}
func TestKeyXMLMarshal(t *testing.T) {
tests := []struct {
name string
key *Key
expectedXML string
}{
{
name: "PLAY key press",
key: NewKey(KeyPlay),
expectedXML: `<key state="press" sender="Gabbo">PLAY</key>`,
},
{
name: "PAUSE key press",
key: NewKey(KeyPause),
expectedXML: `<key state="press" sender="Gabbo">PAUSE</key>`,
},
{
name: "STOP key release",
key: NewKeyRelease(KeyStop),
expectedXML: `<key state="release" sender="Gabbo">STOP</key>`,
},
{
name: "VOLUME_UP key press",
key: NewKey(KeyVolumeUp),
expectedXML: `<key state="press" sender="Gabbo">VOLUME_UP</key>`,
},
{
name: "PRESET_1 key press",
key: NewKey(KeyPreset1),
expectedXML: `<key state="press" sender="Gabbo">PRESET_1</key>`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
xmlData, err := xml.Marshal(tt.key)
if err != nil {
t.Fatalf("Failed to marshal XML: %v", err)
}
if string(xmlData) != tt.expectedXML {
t.Errorf("Expected XML %q, got %q", tt.expectedXML, string(xmlData))
}
})
}
}
func TestKeyXMLUnmarshal(t *testing.T) {
tests := []struct {
name string
xmlData string
want Key
}{
{
name: "PLAY key press",
xmlData: `<key state="press" sender="Gabbo">PLAY</key>`,
want: Key{
State: KeyStatePress,
Sender: "Gabbo",
Value: KeyPlay,
},
},
{
name: "PAUSE key release",
xmlData: `<key state="release" sender="TestSender">PAUSE</key>`,
want: Key{
State: KeyStateRelease,
Sender: "TestSender",
Value: KeyPause,
},
},
{
name: "VOLUME_DOWN key press",
xmlData: `<key state="press" sender="WebApp">VOLUME_DOWN</key>`,
want: Key{
State: KeyStatePress,
Sender: "WebApp",
Value: KeyVolumeDown,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var key Key
err := xml.Unmarshal([]byte(tt.xmlData), &key)
if err != nil {
t.Fatalf("Failed to unmarshal XML: %v", err)
}
if key.State != tt.want.State {
t.Errorf("Expected state %q, got %q", tt.want.State, key.State)
}
if key.Sender != tt.want.Sender {
t.Errorf("Expected sender %q, got %q", tt.want.Sender, key.Sender)
}
if key.Value != tt.want.Value {
t.Errorf("Expected value %q, got %q", tt.want.Value, key.Value)
}
})
}
}
func TestIsValidKey(t *testing.T) {
validKeys := []string{
KeyPlay, KeyPause, KeyStop,
KeyPrevTrack, KeyNextTrack,
KeyVolumeUp, KeyVolumeDown,
KeyPreset1, KeyPreset2, KeyPreset3,
KeyPreset4, KeyPreset5, KeyPreset6,
}
invalidKeys := []string{
"INVALID_KEY", "play", "PAUSE_BUTTON",
"PRESET_7", "PRESET_0", "", "VOLUME",
}
for _, key := range validKeys {
t.Run("valid_"+key, func(t *testing.T) {
if !IsValidKey(key) {
t.Errorf("Expected %q to be valid", key)
}
})
}
for _, key := range invalidKeys {
t.Run("invalid_"+key, func(t *testing.T) {
if IsValidKey(key) {
t.Errorf("Expected %q to be invalid", key)
}
})
}
}
func TestGetAllValidKeys(t *testing.T) {
keys := GetAllValidKeys()
expectedKeys := []string{
KeyPlay, KeyPause, KeyStop,
KeyPrevTrack, KeyNextTrack,
KeyVolumeUp, KeyVolumeDown,
KeyPreset1, KeyPreset2, KeyPreset3,
KeyPreset4, KeyPreset5, KeyPreset6,
}
if len(keys) != len(expectedKeys) {
t.Errorf("Expected %d keys, got %d", len(expectedKeys), len(keys))
}
keyMap := make(map[string]bool)
for _, key := range keys {
keyMap[key] = true
}
for _, expectedKey := range expectedKeys {
if !keyMap[expectedKey] {
t.Errorf("Expected key %q not found in result", expectedKey)
}
}
// Verify all returned keys are valid
for _, key := range keys {
if !IsValidKey(key) {
t.Errorf("GetAllValidKeys returned invalid key: %q", key)
}
}
}
func TestKeyConstants(t *testing.T) {
// Test that all key constants are defined and non-empty
keyTests := []struct {
name string
value string
}{
{"KeyPlay", KeyPlay},
{"KeyPause", KeyPause},
{"KeyStop", KeyStop},
{"KeyPrevTrack", KeyPrevTrack},
{"KeyNextTrack", KeyNextTrack},
{"KeyVolumeUp", KeyVolumeUp},
{"KeyVolumeDown", KeyVolumeDown},
{"KeyPreset1", KeyPreset1},
{"KeyPreset2", KeyPreset2},
{"KeyPreset3", KeyPreset3},
{"KeyPreset4", KeyPreset4},
{"KeyPreset5", KeyPreset5},
{"KeyPreset6", KeyPreset6},
}
for _, tt := range keyTests {
t.Run(tt.name, func(t *testing.T) {
if tt.value == "" {
t.Errorf("Expected %s to be non-empty", tt.name)
}
})
}
}
func TestKeyStateConstants(t *testing.T) {
if KeyStatePress == "" {
t.Error("KeyStatePress should not be empty")
}
if KeyStateRelease == "" {
t.Error("KeyStateRelease should not be empty")
}
if KeyStatePress == KeyStateRelease {
t.Error("KeyStatePress and KeyStateRelease should be different")
}
}
// Benchmark tests
func BenchmarkNewKey(b *testing.B) {
for i := 0; i < b.N; i++ {
NewKey(KeyPlay)
}
}
func BenchmarkKeyXMLMarshal(b *testing.B) {
key := NewKey(KeyPlay)
b.ResetTimer()
for i := 0; i < b.N; i++ {
xml.Marshal(key)
}
}
func BenchmarkIsValidKey(b *testing.B) {
for i := 0; i < b.N; i++ {
IsValidKey(KeyPlay)
}
}