mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat: implement comprehensive /supportedURLs endpoint with feature mapping system
✨ New Features: - Implement missing /supportedURLs endpoint with full XML parsing - Add comprehensive endpoint-to-feature mapping system (15+ features, 9 categories) - Create device capability analysis with personalized recommendations - Add intelligent device classification (Premium, Standard, Basic, Essential, Limited) 🔧 CLI Enhancements: - Add 'supported-urls' command with --features and --verbose flags - Add 'analyze' command for comprehensive device capability analysis - Add 'station list' command for saved station management - Add 'source availability' and 'source compare' commands - Enhanced service availability checking across all commands 📚 Models & API: - New SupportedURLsResponse model with rich helper methods - Enhanced ServiceAvailability model with validation utilities - New EndpointFeature mapping system with CLI command references - Feature completeness scoring and partial implementation detection 🧪 Testing: - 35+ new test cases covering all functionality - Comprehensive feature mapping validation tests - Service availability integration tests with real device scenarios - Mock server tests for error handling and edge cases 📖 Documentation: - New FEATURE-MAPPING-GUIDE.md with comprehensive usage examples - Updated API documentation with correct implementation status - CLI command reference organized by feature category - Device troubleshooting guide with capability checking 🎯 Key Capabilities: - Device feature coverage scoring (0-100%) - Essential vs optional feature classification - Personalized CLI command recommendations - Missing capability detection with usage impact analysis - Smart device type classification based on supported endpoints This resolves the documentation inconsistency where /supportedURLs was marked as implemented but was actually missing from the client. The new implementation goes far beyond basic endpoint listing to provide intelligent device capability analysis and personalized usage recommendations.
This commit is contained in:
+25
-1
@@ -250,6 +250,18 @@ func (c *Client) GetSources() (*models.Sources, error) {
|
||||
return &sources, nil
|
||||
}
|
||||
|
||||
// GetServiceAvailability retrieves service availability status from the /serviceAvailability endpoint
|
||||
func (c *Client) GetServiceAvailability() (*models.ServiceAvailability, error) {
|
||||
var serviceAvailability models.ServiceAvailability
|
||||
|
||||
err := c.get("/serviceAvailability", &serviceAvailability)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service availability: %w", err)
|
||||
}
|
||||
|
||||
return &serviceAvailability, nil
|
||||
}
|
||||
|
||||
// GetName retrieves the device name from the /name endpoint
|
||||
func (c *Client) GetName() (*models.Name, error) {
|
||||
var name models.Name
|
||||
@@ -274,6 +286,18 @@ func (c *Client) GetCapabilities() (*models.Capabilities, error) {
|
||||
return &capabilities, nil
|
||||
}
|
||||
|
||||
// GetSupportedURLs retrieves all supported endpoints from the /supportedURLs endpoint
|
||||
func (c *Client) GetSupportedURLs() (*models.SupportedURLsResponse, error) {
|
||||
var supportedURLs models.SupportedURLsResponse
|
||||
|
||||
err := c.get("/supportedURLs", &supportedURLs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get supported URLs: %w", err)
|
||||
}
|
||||
|
||||
return &supportedURLs, nil
|
||||
}
|
||||
|
||||
// GetPresets retrieves configured presets from the /presets endpoint
|
||||
func (c *Client) GetPresets() (*models.Presets, error) {
|
||||
var presets models.Presets
|
||||
@@ -974,7 +998,7 @@ func (c *Client) post(endpoint string, payload interface{}) error {
|
||||
}
|
||||
|
||||
// postWithResponse performs a POST request with XML body and parses the response
|
||||
func (c *Client) postWithResponse(endpoint string, payload interface{}, result interface{}) error {
|
||||
func (c *Client) postWithResponse(endpoint string, payload, result interface{}) error {
|
||||
url := c.baseURL + endpoint
|
||||
|
||||
var body io.Reader
|
||||
|
||||
@@ -284,3 +284,33 @@ func ExampleClient_GetCapabilities() {
|
||||
// - PRESETS (/presets)
|
||||
// - ZONE (/getZone)
|
||||
}
|
||||
|
||||
func ExampleClient_GetSupportedURLs_concept() {
|
||||
// Example of how to use GetSupportedURLs() method
|
||||
// Note: This example shows the concept but doesn't execute to avoid requiring a real device
|
||||
|
||||
config := &client.Config{Host: "192.168.1.100"}
|
||||
c := client.NewClient(config)
|
||||
|
||||
supportedURLs, err := c.GetSupportedURLs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Device %s supports %d endpoints\n", supportedURLs.DeviceID, supportedURLs.GetURLCount())
|
||||
fmt.Printf("Core functionality: %v\n", supportedURLs.HasCorePlaybackSupport())
|
||||
fmt.Printf("Multiroom support: %v\n", supportedURLs.HasMultiroomSupport())
|
||||
fmt.Printf("Streaming support: %v\n", supportedURLs.HasStreamingSupport())
|
||||
|
||||
// Check specific endpoints
|
||||
if supportedURLs.HasURL("/audiodspcontrols") {
|
||||
fmt.Println("Device supports advanced audio controls")
|
||||
}
|
||||
|
||||
// Expected output with a real device:
|
||||
// Device 08DF1F0BA325 supports 103 endpoints
|
||||
// Core functionality: true
|
||||
// Multiroom support: true
|
||||
// Streaming support: true
|
||||
// Device supports advanced audio controls
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func ExampleClient_NavigateContainer() {
|
||||
len(tracks), len(subdirs))
|
||||
|
||||
// Show first few tracks
|
||||
for i, track := range tracks[:min(3, len(tracks))] {
|
||||
for i, track := range tracks[:minInt(3, len(tracks))] {
|
||||
fmt.Printf("%d. %s", i+1, track.GetDisplayName())
|
||||
if track.ArtistName != "" {
|
||||
fmt.Printf(" - %s", track.ArtistName)
|
||||
@@ -201,7 +201,7 @@ func Example_searchAndPlayWorkflow() {
|
||||
|
||||
// 2. Show available stations
|
||||
fmt.Printf("Found %d stations:\n", len(stations))
|
||||
for i, station := range stations[:min(5, len(stations))] {
|
||||
for i, station := range stations[:minInt(5, len(stations))] {
|
||||
fmt.Printf("%d. %s", i+1, station.GetDisplayName())
|
||||
if station.Description != "" {
|
||||
fmt.Printf(" - %s", station.Description)
|
||||
@@ -224,7 +224,7 @@ func Example_searchAndPlayWorkflow() {
|
||||
}
|
||||
|
||||
// Helper function for min calculation
|
||||
func min(a, b int) int {
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestClient_Navigate(t *testing.T) {
|
||||
w.WriteHeader(tt.serverStatus)
|
||||
}
|
||||
if tt.serverResponse != "" {
|
||||
w.Write([]byte(tt.serverResponse))
|
||||
_, _ = w.Write([]byte(tt.serverResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
@@ -190,7 +190,7 @@ func TestClient_NavigateWithMenu(t *testing.T) {
|
||||
t.Errorf("Expected sort 'dateCreated', got %s", request.Sort)
|
||||
}
|
||||
|
||||
w.Write([]byte(serverResponse))
|
||||
_, _ = w.Write([]byte(serverResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
@@ -242,7 +242,7 @@ func TestClient_NavigateContainer(t *testing.T) {
|
||||
</navigateResponse>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(serverResponse))
|
||||
_, _ = w.Write([]byte(serverResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
@@ -522,7 +522,7 @@ func TestClient_GetPandoraStations(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify it's calling navigate with the right parameters
|
||||
var request models.NavigateRequest
|
||||
xml.NewDecoder(r.Body).Decode(&request)
|
||||
_ = xml.NewDecoder(r.Body).Decode(&request)
|
||||
|
||||
if request.Source != "PANDORA" {
|
||||
t.Errorf("Expected source PANDORA, got %s", request.Source)
|
||||
@@ -811,7 +811,7 @@ func TestClient_SearchPandoraStations(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify it's calling searchStation with the right parameters
|
||||
var request models.SearchStationRequest
|
||||
xml.NewDecoder(r.Body).Decode(&request)
|
||||
_ = xml.NewDecoder(r.Body).Decode(&request)
|
||||
|
||||
if request.Source != "PANDORA" {
|
||||
t.Errorf("Expected source PANDORA, got %s", request.Source)
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func TestGetServiceAvailability_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// This test requires a real SoundTouch device
|
||||
// Set the SOUNDTOUCH_HOST environment variable to run this test
|
||||
// Example: SOUNDTOUCH_HOST=192.168.1.100 go test -v -run TestGetServiceAvailability_Integration
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
t.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration tests")
|
||||
}
|
||||
|
||||
client := NewClientFromHost(host)
|
||||
|
||||
t.Run("get service availability", func(t *testing.T) {
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get service availability: %v", err)
|
||||
}
|
||||
|
||||
if serviceAvailability == nil {
|
||||
t.Fatal("Service availability response is nil")
|
||||
}
|
||||
|
||||
if serviceAvailability.Services == nil {
|
||||
t.Fatal("Services list is nil")
|
||||
}
|
||||
|
||||
t.Logf("Total services: %d", serviceAvailability.GetServiceCount())
|
||||
t.Logf("Available services: %d", serviceAvailability.GetAvailableServiceCount())
|
||||
t.Logf("Unavailable services: %d", serviceAvailability.GetUnavailableServiceCount())
|
||||
|
||||
// Log all services and their availability
|
||||
if serviceAvailability.Services != nil {
|
||||
for _, service := range serviceAvailability.Services.Service {
|
||||
status := "available"
|
||||
if !service.IsAvailable {
|
||||
status = "unavailable"
|
||||
if service.Reason != "" {
|
||||
status += " (" + service.Reason + ")"
|
||||
}
|
||||
}
|
||||
t.Logf("Service %s: %s", service.Type, status)
|
||||
}
|
||||
}
|
||||
|
||||
// Test convenience methods
|
||||
t.Logf("Has Spotify: %v", serviceAvailability.HasSpotify())
|
||||
t.Logf("Has Bluetooth: %v", serviceAvailability.HasBluetooth())
|
||||
t.Logf("Has AirPlay: %v", serviceAvailability.HasAirPlay())
|
||||
t.Logf("Has Alexa: %v", serviceAvailability.HasAlexa())
|
||||
t.Logf("Has TuneIn: %v", serviceAvailability.HasTuneIn())
|
||||
t.Logf("Has Pandora: %v", serviceAvailability.HasPandora())
|
||||
t.Logf("Has Local Music: %v", serviceAvailability.HasLocalMusic())
|
||||
|
||||
// Test service categorization
|
||||
streamingServices := serviceAvailability.GetStreamingServices()
|
||||
t.Logf("Streaming services count: %d", len(streamingServices))
|
||||
for _, service := range streamingServices {
|
||||
t.Logf(" - Streaming: %s (%v)", service.Type, service.IsAvailable)
|
||||
}
|
||||
|
||||
localServices := serviceAvailability.GetLocalServices()
|
||||
t.Logf("Local services count: %d", len(localServices))
|
||||
for _, service := range localServices {
|
||||
t.Logf(" - Local: %s (%v)", service.Type, service.IsAvailable)
|
||||
}
|
||||
|
||||
// Validate that we have at least some services
|
||||
if serviceAvailability.GetServiceCount() == 0 {
|
||||
t.Error("Expected at least one service in the response")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare with sources endpoint", func(t *testing.T) {
|
||||
// Get service availability
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get service availability: %v", err)
|
||||
}
|
||||
|
||||
// Get sources for comparison
|
||||
sources, err := client.GetSources()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get sources: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Comparing service availability with sources endpoint...")
|
||||
|
||||
// Compare Spotify availability
|
||||
spotifyAvailable := serviceAvailability.HasSpotify()
|
||||
spotifyInSources := sources.HasSpotify()
|
||||
t.Logf("Spotify - ServiceAvailability: %v, Sources: %v", spotifyAvailable, spotifyInSources)
|
||||
|
||||
// Compare Bluetooth availability
|
||||
bluetoothAvailable := serviceAvailability.HasBluetooth()
|
||||
bluetoothInSources := sources.HasBluetooth()
|
||||
t.Logf("Bluetooth - ServiceAvailability: %v, Sources: %v", bluetoothAvailable, bluetoothInSources)
|
||||
|
||||
// Compare AUX availability (not directly comparable but useful info)
|
||||
auxInSources := sources.HasAux()
|
||||
t.Logf("AUX in Sources: %v (no direct equivalent in ServiceAvailability)", auxInSources)
|
||||
|
||||
// Note: ServiceAvailability and Sources may not always match perfectly
|
||||
// ServiceAvailability shows what services are theoretically available
|
||||
// Sources shows what sources are currently configured and ready
|
||||
t.Logf("Note: ServiceAvailability shows theoretical availability, Sources shows current configuration")
|
||||
})
|
||||
|
||||
t.Run("validate specific service details", func(t *testing.T) {
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get service availability: %v", err)
|
||||
}
|
||||
|
||||
// Test getting specific services
|
||||
spotifyService := serviceAvailability.GetServiceByType(models.ServiceTypeSpotify)
|
||||
if spotifyService != nil {
|
||||
t.Logf("Spotify service details: Available=%v, Reason=%s",
|
||||
spotifyService.IsAvailable, spotifyService.Reason)
|
||||
|
||||
if !spotifyService.IsType(models.ServiceTypeSpotify) {
|
||||
t.Error("Spotify service type check failed")
|
||||
}
|
||||
} else {
|
||||
t.Log("Spotify service not found in response")
|
||||
}
|
||||
|
||||
bluetoothService := serviceAvailability.GetServiceByType(models.ServiceTypeBluetooth)
|
||||
if bluetoothService != nil {
|
||||
t.Logf("Bluetooth service details: Available=%v, Reason=%s",
|
||||
bluetoothService.IsAvailable, bluetoothService.Reason)
|
||||
} else {
|
||||
t.Log("Bluetooth service not found in response")
|
||||
}
|
||||
|
||||
// Check for services that commonly have reasons when unavailable
|
||||
unavailableServices := serviceAvailability.GetUnavailableServices()
|
||||
for _, service := range unavailableServices {
|
||||
if service.Reason != "" {
|
||||
t.Logf("Service %s is unavailable: %s", service.Type, service.Reason)
|
||||
} else {
|
||||
t.Logf("Service %s is unavailable (no reason provided)", service.Type)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetServiceAvailability_UserFeedback(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
host := os.Getenv("SOUNDTOUCH_TEST_HOST")
|
||||
if host == "" {
|
||||
t.Skip("SOUNDTOUCH_TEST_HOST environment variable not set - skipping integration tests")
|
||||
}
|
||||
|
||||
client := NewClientFromHost(host)
|
||||
|
||||
t.Run("generate user feedback about supported services", func(t *testing.T) {
|
||||
serviceAvailability, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get service availability: %v", err)
|
||||
}
|
||||
|
||||
// Example of how this could be used for user feedback
|
||||
t.Log("\n=== SERVICE AVAILABILITY REPORT ===")
|
||||
|
||||
availableServices := serviceAvailability.GetAvailableServices()
|
||||
if len(availableServices) > 0 {
|
||||
t.Log("\nAvailable Services:")
|
||||
for _, service := range availableServices {
|
||||
t.Logf(" ✅ %s", formatServiceName(service.Type))
|
||||
}
|
||||
}
|
||||
|
||||
unavailableServices := serviceAvailability.GetUnavailableServices()
|
||||
if len(unavailableServices) > 0 {
|
||||
t.Log("\nUnavailable Services:")
|
||||
for _, service := range unavailableServices {
|
||||
reason := ""
|
||||
if service.Reason != "" {
|
||||
reason = " - " + service.Reason
|
||||
}
|
||||
t.Logf(" ❌ %s%s", formatServiceName(service.Type), reason)
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming services summary
|
||||
streamingServices := serviceAvailability.GetStreamingServices()
|
||||
availableStreaming := 0
|
||||
for _, service := range streamingServices {
|
||||
if service.IsAvailable {
|
||||
availableStreaming++
|
||||
}
|
||||
}
|
||||
t.Logf("\nStreaming Services: %d/%d available", availableStreaming, len(streamingServices))
|
||||
|
||||
// Local services summary
|
||||
localServices := serviceAvailability.GetLocalServices()
|
||||
availableLocal := 0
|
||||
for _, service := range localServices {
|
||||
if service.IsAvailable {
|
||||
availableLocal++
|
||||
}
|
||||
}
|
||||
t.Logf("Local Input Services: %d/%d available", availableLocal, len(localServices))
|
||||
|
||||
t.Log("\n=== END REPORT ===")
|
||||
})
|
||||
}
|
||||
|
||||
// formatServiceName converts service type constants to user-friendly names
|
||||
func formatServiceName(serviceType string) string {
|
||||
switch serviceType {
|
||||
case "SPOTIFY":
|
||||
return "Spotify"
|
||||
case "BLUETOOTH":
|
||||
return "Bluetooth"
|
||||
case "AIRPLAY":
|
||||
return "AirPlay"
|
||||
case "ALEXA":
|
||||
return "Amazon Alexa"
|
||||
case "AMAZON":
|
||||
return "Amazon Music"
|
||||
case "PANDORA":
|
||||
return "Pandora"
|
||||
case "TUNEIN":
|
||||
return "TuneIn Radio"
|
||||
case "DEEZER":
|
||||
return "Deezer"
|
||||
case "IHEART":
|
||||
return "iHeartRadio"
|
||||
case "LOCAL_INTERNET_RADIO":
|
||||
return "Internet Radio"
|
||||
case "LOCAL_MUSIC":
|
||||
return "Local Music Library"
|
||||
case "BMX":
|
||||
return "BMX"
|
||||
case "NOTIFICATION":
|
||||
return "Notifications"
|
||||
default:
|
||||
return serviceType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func TestGetServiceAvailability(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responseBody string
|
||||
statusCode int
|
||||
expectError bool
|
||||
validate func(t *testing.T, sa *models.ServiceAvailability)
|
||||
}{
|
||||
{
|
||||
name: "successful response with mixed availability",
|
||||
responseBody: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<serviceAvailability>
|
||||
<services>
|
||||
<service type="AIRPLAY" isAvailable="true" />
|
||||
<service type="ALEXA" isAvailable="false" />
|
||||
<service type="AMAZON" isAvailable="true" />
|
||||
<service type="BLUETOOTH" isAvailable="false" reason="INVALID_SOURCE_TYPE" />
|
||||
<service type="BMX" isAvailable="false" />
|
||||
<service type="DEEZER" isAvailable="true" />
|
||||
<service type="IHEART" isAvailable="true" />
|
||||
<service type="LOCAL_INTERNET_RADIO" isAvailable="true" />
|
||||
<service type="LOCAL_MUSIC" isAvailable="true" />
|
||||
<service type="NOTIFICATION" isAvailable="false" />
|
||||
<service type="PANDORA" isAvailable="true" />
|
||||
<service type="SPOTIFY" isAvailable="true" />
|
||||
<service type="TUNEIN" isAvailable="true" />
|
||||
</services>
|
||||
</serviceAvailability>`,
|
||||
statusCode: 200,
|
||||
expectError: false,
|
||||
validate: func(t *testing.T, sa *models.ServiceAvailability) {
|
||||
t.Helper()
|
||||
if sa == nil {
|
||||
t.Fatal("service availability should not be nil")
|
||||
}
|
||||
if sa.Services == nil {
|
||||
t.Fatal("services should not be nil")
|
||||
}
|
||||
|
||||
// Check total service count
|
||||
if sa.GetServiceCount() != 13 {
|
||||
t.Errorf("expected 13 services, got %d", sa.GetServiceCount())
|
||||
}
|
||||
|
||||
// Check available services count
|
||||
if sa.GetAvailableServiceCount() != 9 {
|
||||
t.Errorf("expected 9 available services, got %d", sa.GetAvailableServiceCount())
|
||||
}
|
||||
|
||||
// Check unavailable services count
|
||||
if sa.GetUnavailableServiceCount() != 4 {
|
||||
t.Errorf("expected 4 unavailable services, got %d", sa.GetUnavailableServiceCount())
|
||||
}
|
||||
|
||||
// Check specific service availability
|
||||
if !sa.HasSpotify() {
|
||||
t.Error("should have Spotify")
|
||||
}
|
||||
if !sa.HasAirPlay() {
|
||||
t.Error("should have AirPlay")
|
||||
}
|
||||
if !sa.HasTuneIn() {
|
||||
t.Error("should have TuneIn")
|
||||
}
|
||||
if !sa.HasPandora() {
|
||||
t.Error("should have Pandora")
|
||||
}
|
||||
if !sa.HasLocalMusic() {
|
||||
t.Error("should have Local Music")
|
||||
}
|
||||
if sa.HasAlexa() {
|
||||
t.Error("should not have Alexa")
|
||||
}
|
||||
if sa.HasBluetooth() {
|
||||
t.Error("should not have Bluetooth")
|
||||
}
|
||||
|
||||
// Check service with reason
|
||||
bluetoothService := sa.GetServiceByType(models.ServiceTypeBluetooth)
|
||||
if bluetoothService == nil {
|
||||
t.Fatal("bluetooth service should not be nil")
|
||||
}
|
||||
if bluetoothService.IsAvailable {
|
||||
t.Error("bluetooth service should not be available")
|
||||
}
|
||||
if bluetoothService.GetReason() != "INVALID_SOURCE_TYPE" {
|
||||
t.Errorf("expected bluetooth reason 'INVALID_SOURCE_TYPE', got '%s'", bluetoothService.GetReason())
|
||||
}
|
||||
|
||||
// Check streaming services
|
||||
streamingServices := sa.GetStreamingServices()
|
||||
if len(streamingServices) != 7 {
|
||||
t.Errorf("expected 7 streaming services, got %d", len(streamingServices))
|
||||
}
|
||||
|
||||
// Check local services
|
||||
localServices := sa.GetLocalServices()
|
||||
if len(localServices) != 3 {
|
||||
t.Errorf("expected 3 local services, got %d", len(localServices))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful response with all services available",
|
||||
responseBody: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<serviceAvailability>
|
||||
<services>
|
||||
<service type="SPOTIFY" isAvailable="true" />
|
||||
<service type="BLUETOOTH" isAvailable="true" />
|
||||
<service type="AIRPLAY" isAvailable="true" />
|
||||
</services>
|
||||
</serviceAvailability>`,
|
||||
statusCode: 200,
|
||||
expectError: false,
|
||||
validate: func(t *testing.T, sa *models.ServiceAvailability) {
|
||||
t.Helper()
|
||||
if sa == nil {
|
||||
t.Fatal("service availability should not be nil")
|
||||
}
|
||||
if sa.Services == nil {
|
||||
t.Fatal("services should not be nil")
|
||||
}
|
||||
|
||||
if sa.GetServiceCount() != 3 {
|
||||
t.Errorf("expected 3 services, got %d", sa.GetServiceCount())
|
||||
}
|
||||
if sa.GetAvailableServiceCount() != 3 {
|
||||
t.Errorf("expected 3 available services, got %d", sa.GetAvailableServiceCount())
|
||||
}
|
||||
if sa.GetUnavailableServiceCount() != 0 {
|
||||
t.Errorf("expected 0 unavailable services, got %d", sa.GetUnavailableServiceCount())
|
||||
}
|
||||
|
||||
if !sa.HasSpotify() {
|
||||
t.Error("should have Spotify")
|
||||
}
|
||||
if !sa.HasBluetooth() {
|
||||
t.Error("should have Bluetooth")
|
||||
}
|
||||
if !sa.HasAirPlay() {
|
||||
t.Error("should have AirPlay")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful response with no services",
|
||||
responseBody: `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<serviceAvailability>
|
||||
<services>
|
||||
</services>
|
||||
</serviceAvailability>`,
|
||||
statusCode: 200,
|
||||
expectError: false,
|
||||
validate: func(t *testing.T, sa *models.ServiceAvailability) {
|
||||
t.Helper()
|
||||
if sa == nil {
|
||||
t.Fatal("service availability should not be nil")
|
||||
}
|
||||
if sa.Services == nil {
|
||||
t.Fatal("services should not be nil")
|
||||
}
|
||||
|
||||
if sa.GetServiceCount() != 0 {
|
||||
t.Errorf("expected 0 services, got %d", sa.GetServiceCount())
|
||||
}
|
||||
if sa.GetAvailableServiceCount() != 0 {
|
||||
t.Errorf("expected 0 available services, got %d", sa.GetAvailableServiceCount())
|
||||
}
|
||||
if sa.GetUnavailableServiceCount() != 0 {
|
||||
t.Errorf("expected 0 unavailable services, got %d", sa.GetUnavailableServiceCount())
|
||||
}
|
||||
|
||||
if sa.HasSpotify() {
|
||||
t.Error("should not have Spotify")
|
||||
}
|
||||
if sa.HasBluetooth() {
|
||||
t.Error("should not have Bluetooth")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
responseBody: "Internal Server Error",
|
||||
statusCode: 500,
|
||||
expectError: true,
|
||||
validate: func(_ *testing.T, _ *models.ServiceAvailability) {},
|
||||
},
|
||||
{
|
||||
name: "invalid XML",
|
||||
responseBody: "not valid xml",
|
||||
statusCode: 200,
|
||||
expectError: true,
|
||||
validate: func(_ *testing.T, _ *models.ServiceAvailability) {},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/serviceAvailability" {
|
||||
t.Errorf("expected path /serviceAvailability, got %s", r.URL.Path)
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("expected GET method, got %s", r.Method)
|
||||
}
|
||||
|
||||
w.WriteHeader(tt.statusCode)
|
||||
_, _ = fmt.Fprint(w, tt.responseBody)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create client
|
||||
client := createTestClient(server.URL)
|
||||
|
||||
// Execute test
|
||||
result, err := client.GetServiceAvailability()
|
||||
|
||||
// Validate error expectation
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected an error but got none")
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil result on error")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
tt.validate(t, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetServiceAvailability_NetworkError(t *testing.T) {
|
||||
// Create client with invalid host
|
||||
client := createTestClient("http://invalid-host:99999")
|
||||
|
||||
result, err := client.GetServiceAvailability()
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error but got none")
|
||||
}
|
||||
if result != nil {
|
||||
t.Error("expected nil result on error")
|
||||
}
|
||||
if err != nil && !contains(err.Error(), "failed to get service availability") {
|
||||
t.Errorf("error message should contain 'failed to get service availability', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceAvailabilityModel_EdgeCases(t *testing.T) {
|
||||
t.Run("nil services", func(t *testing.T) {
|
||||
sa := &models.ServiceAvailability{}
|
||||
|
||||
if sa.GetServiceCount() != 0 {
|
||||
t.Errorf("expected 0 service count, got %d", sa.GetServiceCount())
|
||||
}
|
||||
if sa.GetAvailableServiceCount() != 0 {
|
||||
t.Errorf("expected 0 available count, got %d", sa.GetAvailableServiceCount())
|
||||
}
|
||||
if sa.GetUnavailableServiceCount() != 0 {
|
||||
t.Errorf("expected 0 unavailable count, got %d", sa.GetUnavailableServiceCount())
|
||||
}
|
||||
if sa.HasSpotify() {
|
||||
t.Error("should not have Spotify")
|
||||
}
|
||||
if sa.GetServiceByType(models.ServiceTypeSpotify) != nil {
|
||||
t.Error("service should be nil")
|
||||
}
|
||||
if len(sa.GetAvailableServices()) != 0 {
|
||||
t.Error("available services should be empty")
|
||||
}
|
||||
if len(sa.GetUnavailableServices()) != 0 {
|
||||
t.Error("unavailable services should be empty")
|
||||
}
|
||||
if len(sa.GetStreamingServices()) != 0 {
|
||||
t.Error("streaming services should be empty")
|
||||
}
|
||||
if len(sa.GetLocalServices()) != 0 {
|
||||
t.Error("local services should be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("service type checking", func(t *testing.T) {
|
||||
service := models.Service{
|
||||
Type: "SPOTIFY",
|
||||
IsAvailable: true,
|
||||
}
|
||||
|
||||
if !service.IsType(models.ServiceTypeSpotify) {
|
||||
t.Error("service should be of type Spotify")
|
||||
}
|
||||
if service.IsType(models.ServiceTypeBluetooth) {
|
||||
t.Error("service should not be of type Bluetooth")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("service reason handling", func(t *testing.T) {
|
||||
serviceWithReason := models.Service{
|
||||
Type: "BLUETOOTH",
|
||||
IsAvailable: false,
|
||||
Reason: "DEVICE_NOT_CONNECTED",
|
||||
}
|
||||
|
||||
serviceWithoutReason := models.Service{
|
||||
Type: "SPOTIFY",
|
||||
IsAvailable: true,
|
||||
}
|
||||
|
||||
if serviceWithReason.GetReason() != "DEVICE_NOT_CONNECTED" {
|
||||
t.Errorf("expected DEVICE_NOT_CONNECTED, got %s", serviceWithReason.GetReason())
|
||||
}
|
||||
if serviceWithoutReason.GetReason() != "" {
|
||||
t.Errorf("expected empty reason, got %s", serviceWithoutReason.GetReason())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGetServiceAvailability(b *testing.B) {
|
||||
responseBody := `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<serviceAvailability>
|
||||
<services>
|
||||
<service type="SPOTIFY" isAvailable="true" />
|
||||
<service type="BLUETOOTH" isAvailable="false" reason="UNAVAILABLE" />
|
||||
<service type="AIRPLAY" isAvailable="true" />
|
||||
<service type="PANDORA" isAvailable="true" />
|
||||
<service type="TUNEIN" isAvailable="true" />
|
||||
</services>
|
||||
</serviceAvailability>`
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = fmt.Fprint(w, responseBody)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := createTestClient(server.URL)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.GetServiceAvailability()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
func TestClient_GetSupportedURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responseXML string
|
||||
expectedError bool
|
||||
expectedDeviceID string
|
||||
expectedURLCount int
|
||||
expectedURLs []string
|
||||
}{
|
||||
{
|
||||
name: "successful_supported_urls_retrieval",
|
||||
responseXML: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<supportedURLs deviceID="08DF1F0BA325">
|
||||
<URL location="/info" />
|
||||
<URL location="/capabilities" />
|
||||
<URL location="/supportedURLs" />
|
||||
<URL location="/volume" />
|
||||
<URL location="/bass" />
|
||||
<URL location="/balance" />
|
||||
<URL location="/presets" />
|
||||
<URL location="/nowPlaying" />
|
||||
<URL location="/key" />
|
||||
<URL location="/sources" />
|
||||
<URL location="/serviceAvailability" />
|
||||
<URL location="/navigate" />
|
||||
<URL location="/search" />
|
||||
<URL location="/addStation" />
|
||||
<URL location="/removeStation" />
|
||||
<URL location="/clock" />
|
||||
<URL location="/name" />
|
||||
<URL location="/networkInfo" />
|
||||
<URL location="/setZone" />
|
||||
<URL location="/addZoneSlave" />
|
||||
<URL location="/removeZoneSlave" />
|
||||
<URL location="/audiodspcontrols" />
|
||||
<URL location="/audioproducttonecontrols" />
|
||||
<URL location="/audioproductlevelcontrols" />
|
||||
<URL location="/bassCapabilities" />
|
||||
</supportedURLs>`,
|
||||
expectedError: false,
|
||||
expectedDeviceID: "08DF1F0BA325",
|
||||
expectedURLCount: 25,
|
||||
expectedURLs: []string{
|
||||
"/info", "/capabilities", "/supportedURLs", "/volume", "/bass",
|
||||
"/balance", "/presets", "/nowPlaying", "/key", "/sources",
|
||||
"/serviceAvailability", "/navigate", "/search", "/addStation",
|
||||
"/removeStation", "/clock", "/name", "/networkInfo", "/setZone",
|
||||
"/addZoneSlave", "/removeZoneSlave", "/audiodspcontrols",
|
||||
"/audioproducttonecontrols", "/audioproductlevelcontrols", "/bassCapabilities",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minimal_device_supported_urls",
|
||||
responseXML: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<supportedURLs deviceID="12345">
|
||||
<URL location="/info" />
|
||||
<URL location="/capabilities" />
|
||||
<URL location="/volume" />
|
||||
<URL location="/nowPlaying" />
|
||||
<URL location="/key" />
|
||||
</supportedURLs>`,
|
||||
expectedError: false,
|
||||
expectedDeviceID: "12345",
|
||||
expectedURLCount: 5,
|
||||
expectedURLs: []string{"/info", "/capabilities", "/volume", "/nowPlaying", "/key"},
|
||||
},
|
||||
{
|
||||
name: "empty_supported_urls",
|
||||
responseXML: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<supportedURLs deviceID="EMPTY123">
|
||||
</supportedURLs>`,
|
||||
expectedError: false,
|
||||
expectedDeviceID: "EMPTY123",
|
||||
expectedURLCount: 0,
|
||||
expectedURLs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify request
|
||||
if r.URL.Path != "/supportedURLs" {
|
||||
t.Errorf("Expected path '/supportedURLs', got '%s'", r.URL.Path)
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
t.Errorf("Expected GET method, got '%s'", r.Method)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(tt.responseXML))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Parse server URL
|
||||
serverURL, _ := url.Parse(server.URL)
|
||||
host := serverURL.Hostname()
|
||||
port, _ := strconv.Atoi(serverURL.Port())
|
||||
|
||||
// Create client
|
||||
client := NewClient(&Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
// Test GetSupportedURLs
|
||||
supportedURLs, err := client.GetSupportedURLs()
|
||||
|
||||
// Check error expectation
|
||||
if tt.expectedError && err == nil {
|
||||
t.Errorf("Expected error, but got none")
|
||||
}
|
||||
if !tt.expectedError && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectedError {
|
||||
// Verify device ID
|
||||
if supportedURLs.DeviceID != tt.expectedDeviceID {
|
||||
t.Errorf("Expected device ID '%s', got '%s'", tt.expectedDeviceID, supportedURLs.DeviceID)
|
||||
}
|
||||
|
||||
// Verify URL count
|
||||
if supportedURLs.GetURLCount() != tt.expectedURLCount {
|
||||
t.Errorf("Expected %d URLs, got %d", tt.expectedURLCount, supportedURLs.GetURLCount())
|
||||
}
|
||||
|
||||
// Verify specific URLs
|
||||
urls := supportedURLs.GetURLs()
|
||||
if len(urls) != len(tt.expectedURLs) {
|
||||
t.Errorf("Expected %d URLs in list, got %d", len(tt.expectedURLs), len(urls))
|
||||
}
|
||||
|
||||
// Check each expected URL exists
|
||||
for _, expectedURL := range tt.expectedURLs {
|
||||
if !supportedURLs.HasURL(expectedURL) {
|
||||
t.Errorf("Expected URL '%s' not found in supported URLs", expectedURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetSupportedURLs_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Internal Server Error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverURL, _ := url.Parse(server.URL)
|
||||
host := serverURL.Hostname()
|
||||
port, _ := strconv.Atoi(serverURL.Port())
|
||||
|
||||
client := NewClient(&Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
// Test GetSupportedURLs with server error
|
||||
supportedURLs, err := client.GetSupportedURLs()
|
||||
|
||||
// Should return error
|
||||
if err == nil {
|
||||
t.Error("Expected error for server error response, but got none")
|
||||
}
|
||||
if supportedURLs != nil {
|
||||
t.Error("Expected nil supportedURLs on error, but got result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetSupportedURLs_NotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("Not Found"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverURL, _ := url.Parse(server.URL)
|
||||
host := serverURL.Hostname()
|
||||
port, _ := strconv.Atoi(serverURL.Port())
|
||||
|
||||
client := NewClient(&Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
// Test GetSupportedURLs with 404 response
|
||||
supportedURLs, err := client.GetSupportedURLs()
|
||||
|
||||
// Should return error
|
||||
if err == nil {
|
||||
t.Error("Expected error for 404 response, but got none")
|
||||
}
|
||||
if supportedURLs != nil {
|
||||
t.Error("Expected nil supportedURLs on error, but got result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_GetSupportedURLs_InvalidXML(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("invalid xml content"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverURL, _ := url.Parse(server.URL)
|
||||
host := serverURL.Hostname()
|
||||
port, _ := strconv.Atoi(serverURL.Port())
|
||||
|
||||
client := NewClient(&Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
// Test GetSupportedURLs with invalid XML
|
||||
supportedURLs, err := client.GetSupportedURLs()
|
||||
|
||||
// Should return error
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid XML, but got none")
|
||||
}
|
||||
if supportedURLs != nil {
|
||||
t.Error("Expected nil supportedURLs on error, but got result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedURLsResponse_Methods(t *testing.T) {
|
||||
// Create test data
|
||||
supportedURLs := &models.SupportedURLsResponse{
|
||||
DeviceID: "TEST123",
|
||||
URLs: []models.URL{
|
||||
{Location: "/info"},
|
||||
{Location: "/capabilities"},
|
||||
{Location: "/volume"},
|
||||
{Location: "/bass"},
|
||||
{Location: "/balance"},
|
||||
{Location: "/presets"},
|
||||
{Location: "/nowPlaying"},
|
||||
{Location: "/key"},
|
||||
{Location: "/sources"},
|
||||
{Location: "/navigate"},
|
||||
{Location: "/search"},
|
||||
{Location: "/audiodspcontrols"},
|
||||
{Location: "/setZone"},
|
||||
{Location: "/networkInfo"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("GetURLs", func(t *testing.T) {
|
||||
urls := supportedURLs.GetURLs()
|
||||
if len(urls) != 14 {
|
||||
t.Errorf("Expected 14 URLs, got %d", len(urls))
|
||||
}
|
||||
if urls[0] != "/info" {
|
||||
t.Errorf("Expected first URL to be '/info', got '%s'", urls[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasURL", func(t *testing.T) {
|
||||
if !supportedURLs.HasURL("/info") {
|
||||
t.Error("Expected '/info' to be found")
|
||||
}
|
||||
if !supportedURLs.HasURL("/capabilities") {
|
||||
t.Error("Expected '/capabilities' to be found")
|
||||
}
|
||||
if supportedURLs.HasURL("/nonexistent") {
|
||||
t.Error("Expected '/nonexistent' not to be found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetURLCount", func(t *testing.T) {
|
||||
count := supportedURLs.GetURLCount()
|
||||
if count != 14 {
|
||||
t.Errorf("Expected URL count to be 14, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetCoreURLs", func(t *testing.T) {
|
||||
coreURLs := supportedURLs.GetCoreURLs()
|
||||
expectedCore := []string{"/info", "/capabilities", "/sources", "/volume", "/bass", "/balance", "/presets", "/nowPlaying", "/key"}
|
||||
if len(coreURLs) != len(expectedCore) {
|
||||
t.Errorf("Expected %d core URLs, got %d", len(expectedCore), len(coreURLs))
|
||||
}
|
||||
for _, url := range expectedCore {
|
||||
found := false
|
||||
for _, core := range coreURLs {
|
||||
if core == url {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected core URL '%s' not found", url)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetStreamingURLs", func(t *testing.T) {
|
||||
streamingURLs := supportedURLs.GetStreamingURLs()
|
||||
expectedStreaming := []string{"/navigate", "/search", "/sources"}
|
||||
if len(streamingURLs) != len(expectedStreaming) {
|
||||
t.Errorf("Expected %d streaming URLs, got %d", len(expectedStreaming), len(streamingURLs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetAdvancedURLs", func(t *testing.T) {
|
||||
advancedURLs := supportedURLs.GetAdvancedURLs()
|
||||
expectedAdvanced := []string{"/audiodspcontrols", "/setZone"}
|
||||
if len(advancedURLs) != len(expectedAdvanced) {
|
||||
t.Errorf("Expected %d advanced URLs, got %d", len(expectedAdvanced), len(advancedURLs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetNetworkURLs", func(t *testing.T) {
|
||||
networkURLs := supportedURLs.GetNetworkURLs()
|
||||
expectedNetwork := []string{"/networkInfo"}
|
||||
if len(networkURLs) != len(expectedNetwork) {
|
||||
t.Errorf("Expected %d network URLs, got %d", len(expectedNetwork), len(networkURLs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasCorePlaybackSupport", func(t *testing.T) {
|
||||
if !supportedURLs.HasCorePlaybackSupport() {
|
||||
t.Error("Expected device to have core playback support")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasPresetSupport", func(t *testing.T) {
|
||||
if !supportedURLs.HasPresetSupport() {
|
||||
t.Error("Expected device to have preset support")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasMultiroomSupport", func(t *testing.T) {
|
||||
if !supportedURLs.HasMultiroomSupport() {
|
||||
t.Error("Expected device to have multiroom support")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasAdvancedAudioSupport", func(t *testing.T) {
|
||||
if !supportedURLs.HasAdvancedAudioSupport() {
|
||||
t.Error("Expected device to have advanced audio support")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasStreamingSupport", func(t *testing.T) {
|
||||
if !supportedURLs.HasStreamingSupport() {
|
||||
t.Error("Expected device to have streaming support")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetUnsupportedURLs", func(t *testing.T) {
|
||||
checkList := []string{"/info", "/nonexistent1", "/capabilities", "/nonexistent2"}
|
||||
unsupported := supportedURLs.GetUnsupportedURLs(checkList)
|
||||
expectedUnsupported := []string{"/nonexistent1", "/nonexistent2"}
|
||||
if len(unsupported) != len(expectedUnsupported) {
|
||||
t.Errorf("Expected %d unsupported URLs, got %d", len(expectedUnsupported), len(unsupported))
|
||||
}
|
||||
for _, url := range expectedUnsupported {
|
||||
found := false
|
||||
for _, unsup := range unsupported {
|
||||
if unsup == url {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected unsupported URL '%s' not found", url)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSupportedURLsResponse_EmptyURLs(t *testing.T) {
|
||||
// Test with empty URL list
|
||||
supportedURLs := &models.SupportedURLsResponse{
|
||||
DeviceID: "EMPTY",
|
||||
URLs: []models.URL{},
|
||||
}
|
||||
|
||||
t.Run("empty_urls_basic_checks", func(t *testing.T) {
|
||||
if supportedURLs.GetURLCount() != 0 {
|
||||
t.Errorf("Expected 0 URLs, got %d", supportedURLs.GetURLCount())
|
||||
}
|
||||
if supportedURLs.HasURL("/info") {
|
||||
t.Error("Expected '/info' not to be found in empty list")
|
||||
}
|
||||
if supportedURLs.HasCorePlaybackSupport() {
|
||||
t.Error("Expected no core playback support with empty URLs")
|
||||
}
|
||||
if supportedURLs.HasPresetSupport() {
|
||||
t.Error("Expected no preset support with empty URLs")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSupportedURLsResponse_FeatureMapping(t *testing.T) {
|
||||
// Create test data with comprehensive feature set
|
||||
supportedURLs := &models.SupportedURLsResponse{
|
||||
DeviceID: "FEATURE_TEST",
|
||||
URLs: []models.URL{
|
||||
// Core features
|
||||
{Location: "/info"},
|
||||
{Location: "/capabilities"},
|
||||
{Location: "/name"},
|
||||
{Location: "/supportedURLs"},
|
||||
// Audio features
|
||||
{Location: "/volume"},
|
||||
{Location: "/bass"},
|
||||
{Location: "/bassCapabilities"},
|
||||
{Location: "/balance"},
|
||||
{Location: "/audiodspcontrols"},
|
||||
// Playback features
|
||||
{Location: "/nowPlaying"},
|
||||
{Location: "/key"},
|
||||
{Location: "/trackInfo"},
|
||||
// Source features
|
||||
{Location: "/sources"},
|
||||
{Location: "/select"},
|
||||
{Location: "/serviceAvailability"},
|
||||
// Content features
|
||||
{Location: "/navigate"},
|
||||
{Location: "/search"},
|
||||
{Location: "/addStation"},
|
||||
{Location: "/removeStation"},
|
||||
// Preset features
|
||||
{Location: "/presets"},
|
||||
// Multiroom features
|
||||
{Location: "/setZone"},
|
||||
{Location: "/getZone"},
|
||||
{Location: "/addZoneSlave"},
|
||||
// Network features
|
||||
{Location: "/networkInfo"},
|
||||
{Location: "/bluetoothInfo"},
|
||||
// System features
|
||||
{Location: "/clock"},
|
||||
{Location: "/powerManagement"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("GetSupportedFeatures", func(t *testing.T) {
|
||||
features := supportedURLs.GetSupportedFeatures()
|
||||
if len(features) == 0 {
|
||||
t.Error("Expected supported features, got none")
|
||||
}
|
||||
|
||||
// Check for some expected features
|
||||
featureNames := make(map[string]bool)
|
||||
for _, feature := range features {
|
||||
featureNames[feature.Name] = true
|
||||
}
|
||||
|
||||
expectedFeatures := []string{
|
||||
"Device Information",
|
||||
"Volume Control",
|
||||
"Bass Control",
|
||||
"Playback Control",
|
||||
"Audio Sources",
|
||||
"Content Navigation",
|
||||
"Station Management",
|
||||
"Preset Management",
|
||||
"Multiroom Zones",
|
||||
}
|
||||
|
||||
for _, expected := range expectedFeatures {
|
||||
if !featureNames[expected] {
|
||||
t.Errorf("Expected feature '%s' not found in supported features", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetUnsupportedFeatures", func(t *testing.T) {
|
||||
unsupported := supportedURLs.GetUnsupportedFeatures()
|
||||
// With our comprehensive test data, there should be few unsupported features
|
||||
if len(unsupported) > 5 {
|
||||
t.Errorf("Expected few unsupported features, got %d", len(unsupported))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetFeaturesByCategory", func(t *testing.T) {
|
||||
featuresByCategory := supportedURLs.GetFeaturesByCategory()
|
||||
|
||||
expectedCategories := []string{"Core", "Audio", "Playback", "Sources", "Content", "Presets", "Multiroom", "Network", "System"}
|
||||
for _, category := range expectedCategories {
|
||||
if features, exists := featuresByCategory[category]; !exists || len(features) == 0 {
|
||||
t.Errorf("Expected category '%s' to have features", category)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetFeatureCompleteness", func(t *testing.T) {
|
||||
completeness, supported, total := supportedURLs.GetFeatureCompleteness()
|
||||
|
||||
if completeness < 0 || completeness > 100 {
|
||||
t.Errorf("Completeness should be 0-100, got %d", completeness)
|
||||
}
|
||||
|
||||
if supported <= 0 {
|
||||
t.Errorf("Expected some supported features, got %d", supported)
|
||||
}
|
||||
|
||||
if total <= 0 {
|
||||
t.Errorf("Expected some total features, got %d", total)
|
||||
}
|
||||
|
||||
if supported > total {
|
||||
t.Errorf("Supported features (%d) cannot exceed total (%d)", supported, total)
|
||||
}
|
||||
|
||||
// With our comprehensive test data, should have high completeness
|
||||
if completeness < 70 {
|
||||
t.Errorf("Expected high completeness with comprehensive data, got %d%%", completeness)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetMissingEssentialFeatures", func(t *testing.T) {
|
||||
missing := supportedURLs.GetMissingEssentialFeatures()
|
||||
|
||||
// With our comprehensive test data, should have no missing essential features
|
||||
if len(missing) > 0 {
|
||||
t.Errorf("Expected no missing essential features with comprehensive data, got %d", len(missing))
|
||||
for _, feature := range missing {
|
||||
t.Errorf("Missing essential feature: %s", feature.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetPartiallyImplementedFeatures", func(t *testing.T) {
|
||||
partial := supportedURLs.GetPartiallyImplementedFeatures()
|
||||
|
||||
// The result depends on our test data - some features might be partial
|
||||
// This mainly tests that the function doesn't crash
|
||||
for _, feature := range partial {
|
||||
if len(feature.Endpoints) <= 1 {
|
||||
t.Errorf("Partial feature '%s' should have multiple endpoints, got %d", feature.Name, len(feature.Endpoints))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSupportedURLsResponse_FeatureMappingLimitedDevice(t *testing.T) {
|
||||
// Create test data for a limited device
|
||||
limitedURLs := &models.SupportedURLsResponse{
|
||||
DeviceID: "LIMITED_TEST",
|
||||
URLs: []models.URL{
|
||||
{Location: "/info"},
|
||||
{Location: "/volume"},
|
||||
{Location: "/nowPlaying"},
|
||||
{Location: "/key"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("LimitedDevice_GetMissingEssentialFeatures", func(t *testing.T) {
|
||||
missing := limitedURLs.GetMissingEssentialFeatures()
|
||||
|
||||
// Should have some missing essential features
|
||||
if len(missing) == 0 {
|
||||
t.Error("Expected some missing essential features for limited device")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LimitedDevice_GetFeatureCompleteness", func(t *testing.T) {
|
||||
completeness, supported, total := limitedURLs.GetFeatureCompleteness()
|
||||
|
||||
// Should have lower completeness
|
||||
if completeness > 50 {
|
||||
t.Errorf("Expected low completeness for limited device, got %d%%", completeness)
|
||||
}
|
||||
|
||||
if supported == total {
|
||||
t.Error("Limited device should not support all features")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEndpointFeatureMap(t *testing.T) {
|
||||
features := models.GetEndpointFeatureMap()
|
||||
|
||||
t.Run("FeatureMapStructure", func(t *testing.T) {
|
||||
if len(features) == 0 {
|
||||
t.Error("Expected feature map to contain features")
|
||||
}
|
||||
|
||||
for _, feature := range features {
|
||||
if feature.Name == "" {
|
||||
t.Error("Feature should have a name")
|
||||
}
|
||||
if feature.Description == "" {
|
||||
t.Error("Feature should have a description")
|
||||
}
|
||||
if len(feature.Endpoints) == 0 {
|
||||
t.Errorf("Feature '%s' should have at least one endpoint", feature.Name)
|
||||
}
|
||||
if feature.Category == "" {
|
||||
t.Errorf("Feature '%s' should have a category", feature.Name)
|
||||
}
|
||||
if feature.CLICommand == "" {
|
||||
t.Errorf("Feature '%s' should have CLI command info", feature.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FeatureCategories", func(t *testing.T) {
|
||||
categories := make(map[string]bool)
|
||||
for _, feature := range features {
|
||||
categories[feature.Category] = true
|
||||
}
|
||||
|
||||
expectedCategories := []string{"Core", "Audio", "Playback", "Sources", "Content", "Presets", "Multiroom", "Network", "System"}
|
||||
for _, expected := range expectedCategories {
|
||||
if !categories[expected] {
|
||||
t.Errorf("Expected category '%s' not found in feature map", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EssentialFeatures", func(t *testing.T) {
|
||||
essentialCount := 0
|
||||
for _, feature := range features {
|
||||
if feature.Essential {
|
||||
essentialCount++
|
||||
}
|
||||
}
|
||||
|
||||
if essentialCount == 0 {
|
||||
t.Error("Expected some features to be marked as essential")
|
||||
}
|
||||
|
||||
// Should have a reasonable number of essential features
|
||||
if essentialCount > len(features)/2 {
|
||||
t.Errorf("Too many features marked as essential: %d/%d", essentialCount, len(features))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<serviceAvailability>
|
||||
<services>
|
||||
<service type="AIRPLAY" isAvailable="true" />
|
||||
<service type="ALEXA" isAvailable="false" />
|
||||
<service type="AMAZON" isAvailable="true" />
|
||||
<service type="BLUETOOTH" isAvailable="false" reason="INVALID_SOURCE_TYPE" />
|
||||
<service type="BMX" isAvailable="false" />
|
||||
<service type="DEEZER" isAvailable="true" />
|
||||
<service type="IHEART" isAvailable="true" />
|
||||
<service type="LOCAL_INTERNET_RADIO" isAvailable="true" />
|
||||
<service type="LOCAL_MUSIC" isAvailable="true" />
|
||||
<service type="NOTIFICATION" isAvailable="false" />
|
||||
<service type="PANDORA" isAvailable="true" />
|
||||
<service type="SPOTIFY" isAvailable="true" />
|
||||
<service type="TUNEIN" isAvailable="true" />
|
||||
</services>
|
||||
</serviceAvailability>
|
||||
Reference in New Issue
Block a user