mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +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>
412 lines
9.5 KiB
Go
412 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestRecentsCommands(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
expectedOutput []string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "recents list command",
|
|
args: []string{"soundtouch-cli", "--host", "192.0.2.100", "recents", "list"},
|
|
expectedOutput: []string{
|
|
"Getting recently played content",
|
|
"Recent Items Summary:",
|
|
"Recent Items",
|
|
},
|
|
},
|
|
{
|
|
name: "recents filter by source",
|
|
args: []string{"soundtouch-cli", "--host", "192.0.2.100", "recents", "filter", "--source", "SPOTIFY"},
|
|
expectedOutput: []string{
|
|
"Getting filtered recent content",
|
|
"filtered by source: SPOTIFY",
|
|
},
|
|
},
|
|
{
|
|
name: "recents latest command",
|
|
args: []string{"soundtouch-cli", "--host", "192.0.2.100", "recents", "latest"},
|
|
expectedOutput: []string{
|
|
"Getting most recent item",
|
|
"Most Recent Item:",
|
|
},
|
|
},
|
|
{
|
|
name: "recents stats command",
|
|
args: []string{"soundtouch-cli", "--host", "192.0.2.100", "recents", "stats"},
|
|
expectedOutput: []string{
|
|
"Getting recent items statistics",
|
|
"Recent Items Statistics",
|
|
},
|
|
},
|
|
{
|
|
name: "recents missing host",
|
|
args: []string{"soundtouch-cli", "recents", "list"},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Skip actual execution for now - these would need mock HTTP servers
|
|
// This test structure shows how the CLI commands would be tested
|
|
t.Skip("Integration test - requires mock HTTP server setup")
|
|
|
|
// Example of how you would set up the test:
|
|
// app := createTestApp()
|
|
//
|
|
// var buf bytes.Buffer
|
|
// app.Writer = &buf
|
|
// app.ErrWriter = &buf
|
|
//
|
|
// err := app.Run(tt.args)
|
|
//
|
|
// if tt.expectError {
|
|
// if err == nil {
|
|
// t.Error("expected error, got nil")
|
|
// }
|
|
// return
|
|
// }
|
|
//
|
|
// if err != nil {
|
|
// t.Fatalf("unexpected error: %v", err)
|
|
// }
|
|
//
|
|
// output := buf.String()
|
|
// for _, expected := range tt.expectedOutput {
|
|
// if !strings.Contains(output, expected) {
|
|
// t.Errorf("expected output to contain %q, got:\n%s", expected, output)
|
|
// }
|
|
// }
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrintRecentItem(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
item *models.RecentsResponseItem
|
|
detailed bool
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "basic track item",
|
|
item: &models.RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701200000,
|
|
ContentItem: &models.ContentItem{
|
|
Source: "SPOTIFY",
|
|
Type: "track",
|
|
ItemName: "Test Song",
|
|
},
|
|
},
|
|
detailed: false,
|
|
expected: []string{
|
|
"🎵 Test Song",
|
|
"Source: Spotify",
|
|
"Type: track",
|
|
},
|
|
},
|
|
{
|
|
name: "detailed station item",
|
|
item: &models.RecentsResponseItem{
|
|
DeviceID: "device1",
|
|
UTCTime: 1701200000,
|
|
ID: "station123",
|
|
ContentItem: &models.ContentItem{
|
|
Source: "TUNEIN",
|
|
Type: "stationurl",
|
|
ItemName: "Rock FM",
|
|
Location: "tunein:station:s12345",
|
|
SourceAccount: "tunein_account",
|
|
IsPresetable: true,
|
|
},
|
|
},
|
|
detailed: true,
|
|
expected: []string{
|
|
"📻 Rock FM",
|
|
"Source: TuneIn Radio",
|
|
"ID: station123",
|
|
"Can be saved as preset",
|
|
"Location: tunein:station:s12345",
|
|
"Classification: Streaming",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Call the function
|
|
printRecentItem(1, tt.item, tt.detailed)
|
|
|
|
// Restore stdout and read output
|
|
w.Close()
|
|
|
|
os.Stdout = oldStdout
|
|
|
|
var buf bytes.Buffer
|
|
|
|
_, err := buf.ReadFrom(r)
|
|
if err != nil {
|
|
t.Fatalf("failed to read output: %v", err)
|
|
}
|
|
|
|
output := buf.String()
|
|
|
|
// Check expected strings are present
|
|
for _, expected := range tt.expected {
|
|
if !bytes.Contains(buf.Bytes(), []byte(expected)) {
|
|
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetContentTypeIcon(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
item *models.RecentsResponseItem
|
|
expected string
|
|
}{
|
|
{
|
|
name: "track item",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "track"},
|
|
},
|
|
expected: "🎵",
|
|
},
|
|
{
|
|
name: "station item",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "stationurl"},
|
|
},
|
|
expected: "📻",
|
|
},
|
|
{
|
|
name: "playlist item",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "playlist"},
|
|
},
|
|
expected: "📋",
|
|
},
|
|
{
|
|
name: "album item",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "album"},
|
|
},
|
|
expected: "💿",
|
|
},
|
|
{
|
|
name: "container item",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "container"},
|
|
},
|
|
expected: "📁",
|
|
},
|
|
{
|
|
name: "unknown type",
|
|
item: &models.RecentsResponseItem{
|
|
ContentItem: &models.ContentItem{Type: "unknown"},
|
|
},
|
|
expected: "🎶",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := getContentTypeIcon(tt.item)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatSourceForDisplay(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
expected string
|
|
}{
|
|
{"Spotify", "SPOTIFY", "Spotify"},
|
|
{"Local Music", "LOCAL_MUSIC", "Local Music"},
|
|
{"Stored Music", "STORED_MUSIC", "Stored Music"},
|
|
{"TuneIn", "TUNEIN", "TuneIn Radio"},
|
|
{"Pandora", "PANDORA", "Pandora"},
|
|
{"Amazon", "AMAZON", "Amazon Music"},
|
|
{"Deezer", "DEEZER", "Deezer"},
|
|
{"iHeart", "IHEART", "iHeartRadio"},
|
|
{"Bluetooth", "BLUETOOTH", "Bluetooth"},
|
|
{"AUX", "AUX", "AUX Input"},
|
|
{"AirPlay", "AIRPLAY", "AirPlay"},
|
|
{"Unknown", "UNKNOWN_SOURCE", "UNKNOWN_SOURCE"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := formatSourceForDisplay(tt.source)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncateString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
maxLength int
|
|
expected string
|
|
}{
|
|
{
|
|
name: "short string",
|
|
input: "hello",
|
|
maxLength: 10,
|
|
expected: "hello",
|
|
},
|
|
{
|
|
name: "exact length",
|
|
input: "hello",
|
|
maxLength: 5,
|
|
expected: "hello",
|
|
},
|
|
{
|
|
name: "long string",
|
|
input: "this is a very long string that needs truncation",
|
|
maxLength: 20,
|
|
expected: "this is a very lo...",
|
|
},
|
|
{
|
|
name: "very short max length",
|
|
input: "hello world",
|
|
maxLength: 3,
|
|
expected: "...",
|
|
},
|
|
{
|
|
name: "zero length",
|
|
input: "hello",
|
|
maxLength: 0,
|
|
expected: "...",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := truncateString(tt.input, tt.maxLength)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test helper functions that would be used in full integration tests
|
|
func createTestRecentsResponse() *models.RecentsResponse {
|
|
return &models.RecentsResponse{
|
|
Items: []models.RecentsResponseItem{
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1701300000,
|
|
ID: "spotify1",
|
|
ContentItem: &models.ContentItem{
|
|
Source: "SPOTIFY",
|
|
Type: "track",
|
|
Location: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
|
|
SourceAccount: "spotify_user",
|
|
IsPresetable: true,
|
|
ItemName: "Shape of You - Ed Sheeran",
|
|
ContainerArt: "https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96",
|
|
},
|
|
},
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1701200000,
|
|
ID: "local1",
|
|
ContentItem: &models.ContentItem{
|
|
Source: "LOCAL_MUSIC",
|
|
Type: "track",
|
|
Location: "/music/local_song.mp3",
|
|
IsPresetable: false,
|
|
ItemName: "Local Song - Local Artist",
|
|
},
|
|
},
|
|
{
|
|
DeviceID: "1004567890AA",
|
|
UTCTime: 1701100000,
|
|
ID: "tunein1",
|
|
ContentItem: &models.ContentItem{
|
|
Source: "TUNEIN",
|
|
Type: "stationurl",
|
|
Location: "tunein:station:s24939",
|
|
SourceAccount: "tunein",
|
|
IsPresetable: true,
|
|
ItemName: "BBC Radio 1",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestCreateTestRecentsResponse(t *testing.T) {
|
|
response := createTestRecentsResponse()
|
|
|
|
if response == nil {
|
|
t.Fatal("expected response, got nil")
|
|
}
|
|
|
|
if response.GetItemCount() != 3 {
|
|
t.Errorf("expected 3 items, got %d", response.GetItemCount())
|
|
}
|
|
|
|
if response.IsEmpty() {
|
|
t.Error("expected response not to be empty")
|
|
}
|
|
|
|
// Test filtering
|
|
spotifyItems := response.GetSpotifyItems()
|
|
if len(spotifyItems) != 1 {
|
|
t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems))
|
|
}
|
|
|
|
localItems := response.GetLocalMusicItems()
|
|
if len(localItems) != 1 {
|
|
t.Errorf("expected 1 local music item, got %d", len(localItems))
|
|
}
|
|
|
|
tuneInItems := response.GetTuneInItems()
|
|
if len(tuneInItems) != 1 {
|
|
t.Errorf("expected 1 TuneIn item, got %d", len(tuneInItems))
|
|
}
|
|
|
|
tracks := response.GetTracks()
|
|
if len(tracks) != 2 {
|
|
t.Errorf("expected 2 tracks, got %d", len(tracks))
|
|
}
|
|
|
|
stations := response.GetStations()
|
|
if len(stations) != 1 {
|
|
t.Errorf("expected 1 station, got %d", len(stations))
|
|
}
|
|
|
|
presetableItems := response.GetPresetableItems()
|
|
if len(presetableItems) != 2 {
|
|
t.Errorf("expected 2 presetable items, got %d", len(presetableItems))
|
|
}
|
|
}
|