From 2768838badd44dd3c5baa78cc167b926818b334d Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 1 Feb 2026 22:11:12 +0100 Subject: [PATCH] style: apply golangci-lint --fix for all remaining issues Applied automatic fixes using golangci-lint --fix which resolved: - All remaining wsl_v5 whitespace issues (28 issues) - All whitespace formatting issues (1 issue) - Improved code formatting consistency across the entire codebase All tests passing and functionality preserved. --- cmd/soundtouch-cli/cmd_info.go | 22 +++++++ cmd/soundtouch-cli/cmd_navigation.go | 10 ++++ cmd/soundtouch-cli/cmd_playback.go | 7 +++ cmd/soundtouch-cli/cmd_preset.go | 12 ++++ cmd/soundtouch-cli/cmd_source.go | 15 ++++- cmd/soundtouch-cli/cmd_station.go | 27 +++++++++ cmd/soundtouch-cli/common.go | 6 ++ cmd/soundtouch-cli/common_test.go | 1 + cmd/soundtouch-cli/serviceavailability.go | 9 +++ examples/service-availability/main.go | 2 + pkg/client/client.go | 18 ++++++ pkg/client/navigation_examples_test.go | 8 +++ pkg/client/navigation_integration_test.go | 13 ++++ pkg/client/navigation_test.go | 38 +++++++++--- pkg/client/navigation_xml_test.go | 16 ++++- pkg/client/preset_test.go | 11 ++++ .../serviceavailability_integration_test.go | 9 +++ pkg/client/serviceavailability_test.go | 37 +++++++++++- pkg/client/supported_urls_test.go | 28 +++++++++ pkg/models/navigation.go | 3 + pkg/models/navigation_test.go | 32 ++++++++++ pkg/models/serviceavailability.go | 9 +++ pkg/models/serviceavailability_test.go | 59 +++++++++++++++++++ pkg/models/supportedurls.go | 19 ++++++ 24 files changed, 399 insertions(+), 12 deletions(-) diff --git a/cmd/soundtouch-cli/cmd_info.go b/cmd/soundtouch-cli/cmd_info.go index 86684fd..18174d0 100644 --- a/cmd/soundtouch-cli/cmd_info.go +++ b/cmd/soundtouch-cli/cmd_info.go @@ -298,6 +298,7 @@ func printFeatureCategories(featuresByCategory map[string][]models.EndpointFeatu for _, feature := range features { printFeatureStatus(feature, supportedURLs, verbose) } + fmt.Println() } } @@ -315,6 +316,7 @@ func printFeatureStatus(feature models.EndpointFeature, supportedURLs *models.Su if feature.Essential { fmt.Printf(" ⭐") } + fmt.Printf("\n") if verbose { @@ -342,6 +344,7 @@ func printVerboseFeatureDetails(feature models.EndpointFeature, supportedEndpoin if supportedEndpoints < len(feature.Endpoints) { fmt.Printf(" (partial)") } + fmt.Printf("\n") } @@ -353,6 +356,7 @@ func printMissingEssentialFeatures(supportedURLs *models.SupportedURLsResponse) for _, feature := range missingEssential { fmt.Printf(" āŒ %s - %s\n", feature.Name, feature.Description) } + fmt.Println() } } @@ -361,16 +365,20 @@ func printPartiallyImplementedFeatures(supportedURLs *models.SupportedURLsRespon partial := supportedURLs.GetPartiallyImplementedFeatures() if len(partial) > 0 && verbose { fmt.Printf("āš ļø Partially Supported Features:\n") + for _, feature := range partial { fmt.Printf(" 🟔 %s\n", feature.Name) + for _, endpoint := range feature.Endpoints { status := "āŒ" if supportedURLs.HasURL(endpoint) { status = "āœ…" } + fmt.Printf(" %s %s\n", status, endpoint) } } + fmt.Println() } } @@ -387,6 +395,7 @@ func printDetailedEndpoints(supportedURLs *models.SupportedURLsResponse) { for _, url := range coreURLs { fmt.Printf(" • %s\n", url) } + fmt.Println() } @@ -398,6 +407,7 @@ func printDetailedEndpoints(supportedURLs *models.SupportedURLsResponse) { for _, url := range streamingURLs { fmt.Printf(" • %s\n", url) } + fmt.Println() } @@ -409,6 +419,7 @@ func printDetailedEndpoints(supportedURLs *models.SupportedURLsResponse) { for _, url := range advancedURLs { fmt.Printf(" • %s\n", url) } + fmt.Println() } @@ -420,11 +431,13 @@ func printDetailedEndpoints(supportedURLs *models.SupportedURLsResponse) { for _, url := range networkURLs { fmt.Printf(" • %s\n", url) } + fmt.Println() } // Show all supported URLs fmt.Printf("šŸ“ Complete Endpoint List:\n") + allURLs := supportedURLs.GetURLs() for i, url := range allURLs { fmt.Printf(" %3d. %s\n", i+1, url) @@ -449,6 +462,7 @@ func getDeviceAnalysis(c *cli.Context) error { } printDeviceAnalysis(supportedURLs) + return nil } @@ -469,10 +483,12 @@ func printDeviceAnalysis(supportedURLs *models.SupportedURLsResponse) { missingEssential := supportedURLs.GetMissingEssentialFeatures() if len(missingEssential) > 0 { fmt.Printf("āŒ Missing Essential Features:\n") + for _, feature := range missingEssential { fmt.Printf(" • %s - %s\n", feature.Name, feature.Description) fmt.Printf(" Impact: Device may not function properly without this\n") } + fmt.Println() } else { fmt.Printf("āœ… All essential features are supported\n\n") @@ -481,14 +497,17 @@ func printDeviceAnalysis(supportedURLs *models.SupportedURLsResponse) { // Show what works supportedFeatures := supportedURLs.GetSupportedFeatures() fmt.Printf("āœ… Available Features (%d):\n", len(supportedFeatures)) + categoryCount := make(map[string]int) for _, feature := range supportedFeatures { categoryCount[feature.Category]++ } + for category, count := range categoryCount { emoji := getCategoryEmoji(category) fmt.Printf(" %s %s: %d features\n", emoji, category, count) } + fmt.Println() // Show what's missing @@ -499,6 +518,7 @@ func printDeviceAnalysis(supportedURLs *models.SupportedURLsResponse) { for _, feature := range unsupportedFeatures { fmt.Printf(" • %s - %s\n", feature.Name, feature.Description) } + fmt.Println() } @@ -515,8 +535,10 @@ func printDeviceAnalysis(supportedURLs *models.SupportedURLsResponse) { supportedCount++ } } + fmt.Printf(" • %s (%d/%d endpoints)\n", feature.Name, supportedCount, len(feature.Endpoints)) } + fmt.Println() } diff --git a/cmd/soundtouch-cli/cmd_navigation.go b/cmd/soundtouch-cli/cmd_navigation.go index f4aa400..ec7d094 100644 --- a/cmd/soundtouch-cli/cmd_navigation.go +++ b/cmd/soundtouch-cli/cmd_navigation.go @@ -30,6 +30,7 @@ func browseContent(c *cli.Context) error { } printNavigationResults(response, "Content") + return nil } @@ -58,6 +59,7 @@ func browseWithMenu(c *cli.Context) error { } printNavigationResults(response, "Menu Items") + return nil } @@ -93,6 +95,7 @@ func browseContainer(c *cli.Context) error { } printNavigationResults(response, "Container Contents") + return nil } @@ -127,6 +130,7 @@ func browseTuneIn(c *cli.Context) error { } printNavigationResults(response, "TuneIn Stations") + return nil } @@ -155,6 +159,7 @@ func browsePandora(c *cli.Context) error { } printNavigationResults(response, "Pandora Stations") + return nil } @@ -183,6 +188,7 @@ func browseStoredMusic(c *cli.Context) error { } printNavigationResults(response, "Stored Music Library") + return nil } @@ -203,6 +209,7 @@ func printNavigationResults(response *models.NavigateResponse, title string) { } fmt.Printf(" Items:\n") + for i, item := range response.Items { printNavigationItem(item, i+1, response.Source) } @@ -230,9 +237,11 @@ func printContentItemInfo(item models.NavigateItem, responseSource string) { if item.ContentItem.Source != "" && item.ContentItem.Source != responseSource { fmt.Printf(" Source: %s\n", item.ContentItem.Source) } + if item.Type != "" { fmt.Printf(" Type: %s\n", item.Type) } + if item.ContentItem.Location != "" && len(item.ContentItem.Location) < 100 { fmt.Printf(" Location: %s\n", item.ContentItem.Location) } @@ -243,6 +252,7 @@ func printItemMetadata(item models.NavigateItem) { if item.ArtistName != "" { fmt.Printf(" Artist: %s\n", item.ArtistName) } + if item.AlbumName != "" { fmt.Printf(" Album: %s\n", item.AlbumName) } diff --git a/cmd/soundtouch-cli/cmd_playback.go b/cmd/soundtouch-cli/cmd_playback.go index 22180e9..b5eddf0 100644 --- a/cmd/soundtouch-cli/cmd_playback.go +++ b/cmd/soundtouch-cli/cmd_playback.go @@ -45,9 +45,11 @@ func getNowPlaying(c *cli.Context) error { // printBasicPlaybackInfo prints basic source and status information func printBasicPlaybackInfo(nowPlaying *models.NowPlaying) { fmt.Printf(" Source: %s\n", nowPlaying.Source) + if nowPlaying.SourceAccount != "" { fmt.Printf(" Source Account: %s\n", nowPlaying.SourceAccount) } + fmt.Printf(" Status: %s\n", nowPlaying.PlayStatus.String()) } @@ -56,9 +58,11 @@ func printTrackInfo(nowPlaying *models.NowPlaying) { if nowPlaying.Track != "" { fmt.Printf(" Track: %s\n", nowPlaying.Track) } + if nowPlaying.Artist != "" { fmt.Printf(" Artist: %s\n", nowPlaying.Artist) } + if nowPlaying.Album != "" { fmt.Printf(" Album: %s\n", nowPlaying.Album) } @@ -71,6 +75,7 @@ func printTimeInfo(nowPlaying *models.NowPlaying) { } fmt.Printf(" Duration: %s\n", nowPlaying.FormatDuration()) + if nowPlaying.Position != nil { fmt.Printf(" Position: %s\n", nowPlaying.FormatPosition()) } @@ -115,9 +120,11 @@ func printVerboseContentInfo(nowPlaying *models.NowPlaying, verbose bool) { if nowPlaying.ContentItem.Type != "" { fmt.Printf(" Content Type: %s\n", nowPlaying.ContentItem.Type) } + if nowPlaying.ContentItem.ItemName != "" && nowPlaying.ContentItem.ItemName != nowPlaying.Track { fmt.Printf(" Item Name: %s\n", nowPlaying.ContentItem.ItemName) } + fmt.Printf(" Presetable: %t\n", nowPlaying.ContentItem.IsPresetable) } diff --git a/cmd/soundtouch-cli/cmd_preset.go b/cmd/soundtouch-cli/cmd_preset.go index d4ea435..d79b1f3 100644 --- a/cmd/soundtouch-cli/cmd_preset.go +++ b/cmd/soundtouch-cli/cmd_preset.go @@ -41,19 +41,24 @@ func storeCurrentPreset(c *cli.Context) error { PrintError("Current content cannot be saved as preset") fmt.Printf(" Content: %s\n", nowPlaying.Track) fmt.Printf(" Source: %s\n", nowPlaying.Source) + return fmt.Errorf("current content cannot be preset") } // Show what we're about to store fmt.Printf("Current Content:\n") fmt.Printf(" Track: %s\n", nowPlaying.Track) + if nowPlaying.Artist != "" { fmt.Printf(" Artist: %s\n", nowPlaying.Artist) } + if nowPlaying.Album != "" { fmt.Printf(" Album: %s\n", nowPlaying.Album) } + fmt.Printf(" Source: %s\n", nowPlaying.Source) + if nowPlaying.ContentItem.Location != "" { fmt.Printf(" Location: %s\n", nowPlaying.ContentItem.Location) } @@ -105,14 +110,17 @@ func resolveLocationAndMetadata(params *presetParams) error { if params.name == "" { params.name = metadata.Name } + if params.artwork == "" { params.artwork = metadata.Artwork } } } } + params.source = resolvedSource params.location = resolvedLocation + return nil } @@ -121,9 +129,11 @@ func validatePresetParams(params *presetParams) error { if params.source == "" { return fmt.Errorf("source is required (use --source)") } + if params.location == "" { return fmt.Errorf("location is required (use --location)") } + return nil } @@ -160,9 +170,11 @@ func printPresetContent(params *presetParams) { fmt.Printf(" Name: %s\n", params.name) fmt.Printf(" Source: %s\n", params.source) fmt.Printf(" Location: %s\n", params.location) + if params.sourceAccount != "" { fmt.Printf(" Source Account: %s\n", params.sourceAccount) } + if params.itemType != "" { fmt.Printf(" Type: %s\n", params.itemType) } diff --git a/cmd/soundtouch-cli/cmd_source.go b/cmd/soundtouch-cli/cmd_source.go index 6ed1bd5..c6cce90 100644 --- a/cmd/soundtouch-cli/cmd_source.go +++ b/cmd/soundtouch-cli/cmd_source.go @@ -91,6 +91,7 @@ func listSources(c *cli.Context) error { // Show service availability summary fmt.Println() + checker := NewServiceAvailabilityChecker(client) checker.PrintServiceAvailabilitySummary() @@ -231,6 +232,7 @@ func getServiceAvailability(c *cli.Context) error { // Show available services fmt.Printf("\nāœ… Available Services:\n") + availableServices := serviceAvailability.GetAvailableServices() if len(availableServices) == 0 { fmt.Printf(" None\n") @@ -242,6 +244,7 @@ func getServiceAvailability(c *cli.Context) error { // Show unavailable services with reasons fmt.Printf("\nāŒ Unavailable Services:\n") + unavailableServices := serviceAvailability.GetUnavailableServices() if len(unavailableServices) == 0 { fmt.Printf(" None\n") @@ -251,35 +254,44 @@ func getServiceAvailability(c *cli.Context) error { if service.Reason != "" { reason = fmt.Sprintf(" (%s)", service.Reason) } + fmt.Printf(" • %s%s\n", formatServiceTypeForDisplay(models.ServiceType(service.Type)), reason) } } // Show service categories fmt.Printf("\nšŸŽµ Streaming Services:\n") + streamingServices := serviceAvailability.GetStreamingServices() availableCount := 0 + for _, service := range streamingServices { status := "āŒ" if service.IsAvailable { status = "āœ…" availableCount++ } + fmt.Printf(" %s %s\n", status, formatServiceTypeForDisplay(models.ServiceType(service.Type))) } + fmt.Printf(" Summary: %d/%d streaming services available\n", availableCount, len(streamingServices)) fmt.Printf("\nšŸ”— Local Input Services:\n") + localServices := serviceAvailability.GetLocalServices() localAvailableCount := 0 + for _, service := range localServices { status := "āŒ" if service.IsAvailable { status = "āœ…" localAvailableCount++ } + fmt.Printf(" %s %s\n", status, formatServiceTypeForDisplay(models.ServiceType(service.Type))) } + fmt.Printf(" Summary: %d/%d local services available\n", localAvailableCount, len(localServices)) return nil @@ -360,6 +372,7 @@ func compareServiceStatus(serviceName string, configured, available bool, servic default: fmt.Printf(" āž– %s is neither configured nor available\n", serviceName) } + fmt.Println() } @@ -387,7 +400,6 @@ func printSourceSummary(sources *models.Sources, serviceAvailability *models.Ser fmt.Printf(" Ready configured sources: %d\n", sources.GetReadySourceCount()) fmt.Printf(" Total available services: %d\n", serviceAvailability.GetAvailableServiceCount()) fmt.Printf(" Total possible services: %d\n", serviceAvailability.GetServiceCount()) - } // boolToStatus converts boolean to user-friendly status @@ -395,5 +407,6 @@ func boolToStatus(b bool) string { if b { return "āœ… Yes" } + return "āŒ No" } diff --git a/cmd/soundtouch-cli/cmd_station.go b/cmd/soundtouch-cli/cmd_station.go index 1301dba..a47f191 100644 --- a/cmd/soundtouch-cli/cmd_station.go +++ b/cmd/soundtouch-cli/cmd_station.go @@ -43,6 +43,7 @@ func searchStations(c *cli.Context) error { } printSearchResults(response, searchTerm) + return nil } @@ -77,6 +78,7 @@ func searchTuneIn(c *cli.Context) error { } printSearchResults(response, searchTerm) + return nil } @@ -117,6 +119,7 @@ func searchPandora(c *cli.Context) error { } printSearchResults(response, searchTerm) + return nil } @@ -157,6 +160,7 @@ func searchSpotify(c *cli.Context) error { } printSearchResults(response, searchTerm) + return nil } @@ -206,6 +210,7 @@ func addStation(c *cli.Context) error { } PrintSuccess(fmt.Sprintf("Added and started playing station: %s", name)) + return nil } @@ -250,6 +255,7 @@ func removeStation(c *cli.Context) error { } PrintSuccess("Station removed successfully") + return nil } @@ -282,18 +288,23 @@ func printSongs(songs []models.SearchResult) { } fmt.Printf("\n šŸŽµ Songs (%d):\n", len(songs)) + for i := range songs { song := &songs[i] fmt.Printf(" %d. %s\n", i+1, song.GetDisplayName()) + if song.Artist != "" { fmt.Printf(" Artist: %s\n", song.Artist) } + if song.Album != "" { fmt.Printf(" Album: %s\n", song.Album) } + if song.SourceAccount != "" { fmt.Printf(" Account: %s\n", song.SourceAccount) } + fmt.Printf(" Token: %s\n", song.Token) fmt.Println() } @@ -306,12 +317,15 @@ func printArtists(artists []models.SearchResult) { } fmt.Printf(" šŸŽ¤ Artists (%d):\n", len(artists)) + for i := range artists { artist := &artists[i] fmt.Printf(" %d. %s\n", i+1, artist.GetDisplayName()) + if artist.SourceAccount != "" { fmt.Printf(" Account: %s\n", artist.SourceAccount) } + fmt.Printf(" Token: %s\n", artist.Token) fmt.Println() } @@ -324,16 +338,21 @@ func printStations(stations []models.SearchResult) { } fmt.Printf(" šŸ“» Stations (%d):\n", len(stations)) + for i := range stations { station := &stations[i] fmt.Printf(" %d. %s\n", i+1, station.GetDisplayName()) + if station.SourceAccount != "" { fmt.Printf(" Account: %s\n", station.SourceAccount) } + fmt.Printf(" Token: %s\n", station.Token) + if station.Description != "" { fmt.Printf(" Description: %s\n", station.Description) } + fmt.Println() } } @@ -342,9 +361,11 @@ func printStations(stations []models.SearchResult) { func printSearchHints(response *models.SearchStationResponse, songs, artists, stations []models.SearchResult) { fmt.Printf("šŸ’” Usage hints:\n") fmt.Printf(" • To add a station and play it: station add --source %s --token --name \n", response.Source) + if hasAccountResults(response) { fmt.Printf(" • Include --source-account when adding stations that require it\n") } + if len(songs) > 0 || len(artists) > 0 || len(stations) > 0 { fmt.Printf(" • Copy the token from results above to use with 'station add'\n") } @@ -358,6 +379,7 @@ func hasAccountResults(response *models.SearchStationResponse) bool { return true } } + return false } @@ -393,6 +415,7 @@ func listStations(c *cli.Context) error { PrintError("Pandora source account is required") return fmt.Errorf("source account required for Pandora") } + response, err = client.GetPandoraStations(sourceAccount) default: return fmt.Errorf("listing stations is not supported for source: %s", source) @@ -404,6 +427,7 @@ func listStations(c *cli.Context) error { } printStationList(response, source) + return nil } @@ -427,9 +451,11 @@ func printStationList(response *models.NavigateResponse, source string) { if station.ContentItem.Location != "" { fmt.Printf(" Location: %s\n", station.ContentItem.Location) } + if station.ContentItem.SourceAccount != "" { fmt.Printf(" Account: %s\n", station.ContentItem.SourceAccount) } + if station.ContentItem.IsPresetable { fmt.Printf(" Can be saved as preset: Yes\n") } @@ -438,6 +464,7 @@ func printStationList(response *models.NavigateResponse, source string) { if station.Type != "" { fmt.Printf(" Type: %s\n", station.Type) } + fmt.Println() } diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go index efc0dfd..f02de59 100644 --- a/cmd/soundtouch-cli/common.go +++ b/cmd/soundtouch-cli/common.go @@ -146,6 +146,7 @@ func resolveLocation(source, location string) (string, string) { // Example: https://tunein.com/radio/WDR-2-Rheinland-1004-s213886/ if strings.Contains(location, "tunein.com/radio/") { trimmed := strings.TrimSuffix(location, "/") + parts := strings.Split(trimmed, "-") if len(parts) > 0 { lastPart := parts[len(parts)-1] @@ -155,6 +156,7 @@ func resolveLocation(source, location string) (string, string) { } // Fallback for URLs like https://tunein.com/radio/s213886/ parts = strings.Split(trimmed, "/") + lastPart := parts[len(parts)-1] if strings.HasPrefix(lastPart, "s") { return "TUNEIN", "/v1/playback/station/" + lastPart @@ -203,6 +205,7 @@ func fetchTuneInMetadata(url string) (*TuneInMetadata, error) { titlePrefix := `property="og:title" content="` if idx := strings.Index(html, titlePrefix); idx != -1 { start := idx + len(titlePrefix) + end := strings.Index(html[start:], `"`) if end != -1 { title := html[start : start+end] @@ -210,9 +213,11 @@ func fetchTuneInMetadata(url string) (*TuneInMetadata, error) { if pipeIdx := strings.Index(title, " | "); pipeIdx != -1 { title = title[:pipeIdx] } + if commaIdx := strings.Index(title, ", "); commaIdx != -1 { title = title[:commaIdx] } + metadata.Name = title } } @@ -220,6 +225,7 @@ func fetchTuneInMetadata(url string) (*TuneInMetadata, error) { imagePrefix := `property="og:image" content="` if idx := strings.Index(html, imagePrefix); idx != -1 { start := idx + len(imagePrefix) + end := strings.Index(html[start:], `"`) if end != -1 { metadata.Artwork = html[start : start+end] diff --git a/cmd/soundtouch-cli/common_test.go b/cmd/soundtouch-cli/common_test.go index 5a5b807..560446b 100644 --- a/cmd/soundtouch-cli/common_test.go +++ b/cmd/soundtouch-cli/common_test.go @@ -108,6 +108,7 @@ func TestResolveLocation(t *testing.T) { if gotSource != tt.expectedSource { t.Errorf("resolveLocation() gotSource = %v, want %v", gotSource, tt.expectedSource) } + if gotLocation != tt.expectedLocation { t.Errorf("resolveLocation() gotLocation = %v, want %v", gotLocation, tt.expectedLocation) } diff --git a/cmd/soundtouch-cli/serviceavailability.go b/cmd/soundtouch-cli/serviceavailability.go index 6eaaffe..a4e5a7d 100644 --- a/cmd/soundtouch-cli/serviceavailability.go +++ b/cmd/soundtouch-cli/serviceavailability.go @@ -39,6 +39,7 @@ func (sac *ServiceAvailabilityChecker) loadServiceAvailability() { // Create a mock availability that allows everything sac.serviceAvailability = &models.ServiceAvailability{} sac.cached = true + return } @@ -46,6 +47,7 @@ func (sac *ServiceAvailabilityChecker) loadServiceAvailability() { if err != nil { // If availability check fails, warn but don't fail the command PrintWarning(fmt.Sprintf("Could not check service availability: %v", err)) + if !sac.skipAvailabilityCheck { PrintWarning("Command will proceed without availability validation") PrintWarning("Set SOUNDTOUCH_SKIP_AVAILABILITY_CHECK=true to disable these checks") @@ -53,6 +55,7 @@ func (sac *ServiceAvailabilityChecker) loadServiceAvailability() { // Create empty availability to prevent further errors sac.serviceAvailability = &models.ServiceAvailability{} sac.cached = true + return } @@ -94,6 +97,7 @@ func (sac *ServiceAvailabilityChecker) CheckServiceAvailable(serviceType models. sac.suggestAlternatives(serviceType, actionDescription) PrintWarning("To bypass this check, set SOUNDTOUCH_SKIP_AVAILABILITY_CHECK=true") + return false } @@ -241,6 +245,7 @@ func (sac *ServiceAvailabilityChecker) suggestAlternatives(serviceType models.Se if sac.serviceAvailability.HasTuneIn() { PrintWarning("šŸ’” Alternative: TuneIn Radio is available for music streaming") } + if sac.serviceAvailability.HasPandora() { PrintWarning("šŸ’” Alternative: Pandora is available for music streaming") } @@ -249,6 +254,7 @@ func (sac *ServiceAvailabilityChecker) suggestAlternatives(serviceType models.Se if sac.serviceAvailability.HasAirPlay() { PrintWarning("šŸ’” Alternative: AirPlay is available for wireless audio") } + if sac.serviceAvailability.HasLocalMusic() { PrintWarning("šŸ’” Alternative: Local Music Library is available") } @@ -257,6 +263,7 @@ func (sac *ServiceAvailabilityChecker) suggestAlternatives(serviceType models.Se if sac.serviceAvailability.HasSpotify() { PrintWarning("šŸ’” Alternative: Spotify is available for music streaming") } + if sac.serviceAvailability.HasPandora() { PrintWarning("šŸ’” Alternative: Pandora is available for music streaming") } @@ -358,6 +365,7 @@ func (sac *ServiceAvailabilityChecker) PrintServiceAvailabilitySummary() { // Show quick status for popular services fmt.Printf(" Popular services:\n") + popularChecks := []struct { check func() bool name string @@ -374,6 +382,7 @@ func (sac *ServiceAvailabilityChecker) PrintServiceAvailabilitySummary() { if check.check() { status = "āœ…" } + fmt.Printf(" %s %s\n", status, check.name) } diff --git a/examples/service-availability/main.go b/examples/service-availability/main.go index 027be8a..48cf5c5 100644 --- a/examples/service-availability/main.go +++ b/examples/service-availability/main.go @@ -113,8 +113,10 @@ func displayServiceCategories(sa *models.ServiceAvailability) { status = "āœ…" availableCount++ } + fmt.Printf(" %s %s\n", status, formatServiceName(service.Type)) } + fmt.Printf(" Summary: %d/%d streaming services available\n", availableCount, len(streamingServices)) fmt.Println("\nšŸ”— LOCAL INPUT SERVICES:") diff --git a/pkg/client/client.go b/pkg/client/client.go index 66a5485..b5f3ab2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1441,9 +1441,11 @@ func (c *Client) Navigate(source, sourceAccount string, startItem, numItems int) if source == "" { return nil, fmt.Errorf("source cannot be empty") } + if startItem < 1 { return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem) } + if numItems < 1 { return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems) } @@ -1451,6 +1453,7 @@ func (c *Client) Navigate(source, sourceAccount string, startItem, numItems int) request := models.NewNavigateRequest(source, sourceAccount, startItem, numItems) var response models.NavigateResponse + err := c.postWithResponse("/navigate", request, &response) if err != nil { return nil, fmt.Errorf("failed to navigate %s: %w", source, err) @@ -1464,9 +1467,11 @@ func (c *Client) NavigateWithMenu(source, sourceAccount, menu, sort string, star if source == "" { return nil, fmt.Errorf("source cannot be empty") } + if startItem < 1 { return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem) } + if numItems < 1 { return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems) } @@ -1474,6 +1479,7 @@ func (c *Client) NavigateWithMenu(source, sourceAccount, menu, sort string, star request := models.NewNavigateRequestWithMenu(source, sourceAccount, menu, sort, startItem, numItems) var response models.NavigateResponse + err := c.postWithResponse("/navigate", request, &response) if err != nil { return nil, fmt.Errorf("failed to navigate %s with menu %s: %w", source, menu, err) @@ -1487,12 +1493,15 @@ func (c *Client) NavigateContainer(source, sourceAccount string, startItem, numI if source == "" { return nil, fmt.Errorf("source cannot be empty") } + if containerItem == nil { return nil, fmt.Errorf("container item cannot be nil") } + if startItem < 1 { return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem) } + if numItems < 1 { return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems) } @@ -1500,6 +1509,7 @@ func (c *Client) NavigateContainer(source, sourceAccount string, startItem, numI request := models.NewNavigateRequestWithItem(source, sourceAccount, startItem, numItems, containerItem) var response models.NavigateResponse + err := c.postWithResponse("/navigate", request, &response) if err != nil { return nil, fmt.Errorf("failed to navigate container in %s: %w", source, err) @@ -1513,9 +1523,11 @@ func (c *Client) AddStation(source, sourceAccount, token, name string) error { if source == "" { return fmt.Errorf("source cannot be empty") } + if token == "" { return fmt.Errorf("token cannot be empty") } + if name == "" { return fmt.Errorf("station name cannot be empty") } @@ -1523,6 +1535,7 @@ func (c *Client) AddStation(source, sourceAccount, token, name string) error { request := models.NewAddStationRequest(source, sourceAccount, token, name) var response models.StationResponse + err := c.postWithResponse("/addStation", request, &response) if err != nil { return fmt.Errorf("failed to add station '%s' to %s: %w", name, source, err) @@ -1536,14 +1549,17 @@ func (c *Client) RemoveStation(contentItem *models.ContentItem) error { if contentItem == nil { return fmt.Errorf("content item cannot be nil") } + if contentItem.Source == "" { return fmt.Errorf("content item source cannot be empty") } + if contentItem.Location == "" { return fmt.Errorf("content item location cannot be empty") } var response models.StationResponse + err := c.postWithResponse("/removeStation", contentItem, &response) if err != nil { return fmt.Errorf("failed to remove station from %s: %w", contentItem.Source, err) @@ -1580,6 +1596,7 @@ func (c *Client) SearchStation(source, sourceAccount, searchTerm string) (*model if source == "" { return nil, fmt.Errorf("source cannot be empty") } + if searchTerm == "" { return nil, fmt.Errorf("search term cannot be empty") } @@ -1587,6 +1604,7 @@ func (c *Client) SearchStation(source, sourceAccount, searchTerm string) (*model request := models.NewSearchStationRequest(source, sourceAccount, searchTerm) var response models.SearchStationResponse + err := c.postWithResponse("/searchStation", request, &response) if err != nil { return nil, fmt.Errorf("failed to search stations in %s: %w", source, err) diff --git a/pkg/client/navigation_examples_test.go b/pkg/client/navigation_examples_test.go index 180d4f7..998ed09 100644 --- a/pkg/client/navigation_examples_test.go +++ b/pkg/client/navigation_examples_test.go @@ -40,6 +40,7 @@ func ExampleClient_SearchStation() { stations := results.GetStations() for _, station := range stations { fmt.Printf("Station: %s\n", station.GetDisplayName()) + if station.Description != "" { fmt.Printf(" Description: %s\n", station.Description) } @@ -83,6 +84,7 @@ func Example_navigationWorkflow() { // 1. Search for content fmt.Println("Searching for Taylor Swift...") + searchResults, err := client.SearchPandoraStations("user123", "Taylor Swift") if err != nil { log.Fatal(err) @@ -111,6 +113,7 @@ func Example_navigationWorkflow() { // 4. Browse existing Pandora stations fmt.Println("\nBrowsing existing Pandora stations...") + pandoraStations, err := client.GetPandoraStations("user123") if err != nil { fmt.Printf("Could not get Pandora stations: %v\n", err) @@ -173,9 +176,11 @@ func ExampleClient_NavigateContainer() { // Show first few 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) } + fmt.Println() } } @@ -205,9 +210,11 @@ func Example_searchAndPlayWorkflow() { 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) } + fmt.Println() } @@ -230,5 +237,6 @@ func minInt(a, b int) int { if a < b { return a } + return b } diff --git a/pkg/client/navigation_integration_test.go b/pkg/client/navigation_integration_test.go index e9f4b67..87a8365 100644 --- a/pkg/client/navigation_integration_test.go +++ b/pkg/client/navigation_integration_test.go @@ -51,6 +51,7 @@ func TestClient_Navigation_Integration(t *testing.T) { if err != nil { t.Logf("Navigate TUNEIN failed (may not be available): %v", err) t.Skip("TUNEIN not available on test device") + return } @@ -68,6 +69,7 @@ func TestClient_Navigation_Integration(t *testing.T) { if err != nil { t.Logf("GetTuneInStations failed (may not be available): %v", err) t.Skip("TuneIn not available on test device") + return } @@ -86,6 +88,7 @@ func TestClient_Navigation_Integration(t *testing.T) { } var storedMusicAccount string + for _, source := range sources.SourceItem { if source.Source == "STORED_MUSIC" && source.Status.IsReady() { storedMusicAccount = source.SourceAccount @@ -119,6 +122,7 @@ func TestClient_Navigation_Integration(t *testing.T) { if err != nil { t.Logf("SearchTuneInStations failed (may not be supported): %v", err) t.Skip("TuneIn search not supported on test device") + return } @@ -137,6 +141,7 @@ func TestClient_Navigation_Integration(t *testing.T) { if len(stations) > 0 { station := stations[0] t.Logf(" First station: %s", station.GetDisplayName()) + if station.Token != "" { t.Logf(" Station token: %s", station.Token) } @@ -206,6 +211,7 @@ func TestClient_StationManagement_Integration(t *testing.T) { if err != nil { t.Logf("SearchPandoraStations failed: %v", err) t.Skip("Pandora search not working") + return } @@ -321,8 +327,10 @@ func TestClient_Navigation_ErrorHandling_Integration(t *testing.T) { var finalHost string var finalPort int + if strings.Contains(host, ":") { parts := strings.Split(host, ":") + finalHost = parts[0] if len(parts) > 1 { finalPort = 8090 @@ -398,8 +406,10 @@ func BenchmarkClient_Navigate_Integration(b *testing.B) { var finalHost string var finalPort int + if strings.Contains(host, ":") { parts := strings.Split(host, ":") + finalHost = parts[0] if len(parts) > 1 { finalPort = 8090 @@ -426,6 +436,7 @@ func BenchmarkClient_Navigate_Integration(b *testing.B) { if err != nil { b.Logf("Navigate failed: %v", err) b.Skip("TuneIn not available") + return } } @@ -436,10 +447,12 @@ func BenchmarkClient_Navigate_Integration(b *testing.B) { for i := 0; i < b.N; i++ { term := searchTerms[i%len(searchTerms)] + _, err := client.SearchTuneInStations(term) if err != nil { b.Logf("Search failed: %v", err) b.Skip("TuneIn search not available") + return } } diff --git a/pkg/client/navigation_test.go b/pkg/client/navigation_test.go index 8e1e853..a6d809a 100644 --- a/pkg/client/navigation_test.go +++ b/pkg/client/navigation_test.go @@ -117,6 +117,7 @@ func TestClient_Navigate(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -140,6 +141,7 @@ func TestClient_Navigate(t *testing.T) { } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err) } + return } @@ -178,6 +180,7 @@ func TestClient_NavigateWithMenu(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify request body contains menu and sort parameters var request models.NavigateRequest + err := xml.NewDecoder(r.Body).Decode(&request) if err != nil { t.Errorf("Failed to decode request: %v", err) @@ -186,6 +189,7 @@ func TestClient_NavigateWithMenu(t *testing.T) { if request.Menu != "radioStations" { t.Errorf("Expected menu 'radioStations', got %s", request.Menu) } + if request.Sort != "dateCreated" { t.Errorf("Expected sort 'dateCreated', got %s", request.Sort) } @@ -202,8 +206,8 @@ func TestClient_NavigateWithMenu(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.NavigateWithMenu("PANDORA", "user123", "radioStations", "dateCreated", 1, 100) + response, err := client.NavigateWithMenu("PANDORA", "user123", "radioStations", "dateCreated", 1, 100) if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -212,6 +216,7 @@ func TestClient_NavigateWithMenu(t *testing.T) { if response.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", response.Source) } + if response.TotalItems != 5 { t.Errorf("Expected totalItems 5, got %d", response.TotalItems) } @@ -253,8 +258,8 @@ func TestClient_NavigateContainer(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.NavigateContainer("STORED_MUSIC", "device123/0", 1, 1000, containerItem) + response, err := client.NavigateContainer("STORED_MUSIC", "device123/0", 1, 1000, containerItem) if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -341,6 +346,7 @@ func TestClient_AddStation(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -348,6 +354,7 @@ func TestClient_AddStation(t *testing.T) { // Verify request format if !tt.expectError { var request models.AddStationRequest + err := xml.NewDecoder(r.Body).Decode(&request) if err != nil { t.Errorf("Failed to decode request: %v", err) @@ -356,9 +363,11 @@ func TestClient_AddStation(t *testing.T) { if request.Source != tt.source { t.Errorf("Expected source %s, got %s", tt.source, request.Source) } + if request.Token != tt.token { t.Errorf("Expected token %s, got %s", tt.token, request.Token) } + if request.Name != tt.stationName { t.Errorf("Expected name %s, got %s", tt.stationName, request.Name) } @@ -454,6 +463,7 @@ func TestClient_RemoveStation(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -461,6 +471,7 @@ func TestClient_RemoveStation(t *testing.T) { // Verify request format if !tt.expectError && tt.contentItem != nil { var request models.ContentItem + err := xml.NewDecoder(r.Body).Decode(&request) if err != nil { t.Errorf("Failed to decode request: %v", err) @@ -469,6 +480,7 @@ func TestClient_RemoveStation(t *testing.T) { if request.Source != tt.contentItem.Source { t.Errorf("Expected source %s, got %s", tt.contentItem.Source, request.Source) } + if request.Location != tt.contentItem.Location { t.Errorf("Expected location %s, got %s", tt.contentItem.Location, request.Location) } @@ -520,14 +532,17 @@ 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) if request.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", request.Source) } + if request.Menu != "radioStations" { t.Errorf("Expected menu radioStations, got %s", request.Menu) } + if request.Sort != "dateCreated" { t.Errorf("Expected sort dateCreated, got %s", request.Sort) } @@ -544,8 +559,8 @@ func TestClient_GetPandoraStations(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.GetPandoraStations("user123") + response, err := client.GetPandoraStations("user123") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -590,8 +605,8 @@ func TestClient_GetTuneInStations(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.GetTuneInStations("Rock") + response, err := client.GetTuneInStations("Rock") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -630,8 +645,8 @@ func TestClient_GetStoredMusicLibrary(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.GetStoredMusicLibrary("device123/0") + response, err := client.GetStoredMusicLibrary("device123/0") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -733,6 +748,7 @@ func TestClient_SearchStation(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -740,6 +756,7 @@ func TestClient_SearchStation(t *testing.T) { // Verify request format for valid requests if !tt.expectError { var request models.SearchStationRequest + err := xml.NewDecoder(r.Body).Decode(&request) if err != nil { t.Errorf("Failed to decode request: %v", err) @@ -748,6 +765,7 @@ func TestClient_SearchStation(t *testing.T) { if request.Source != tt.source { t.Errorf("Expected source %s, got %s", tt.source, request.Source) } + if request.SearchTerm != tt.searchTerm { t.Errorf("Expected searchTerm %s, got %s", tt.searchTerm, request.SearchTerm) } @@ -771,6 +789,7 @@ func TestClient_SearchStation(t *testing.T) { } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err) } + return } @@ -805,14 +824,17 @@ 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) if request.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", request.Source) } + if request.SourceAccount != "user123" { t.Errorf("Expected sourceAccount user123, got %s", request.SourceAccount) } + if request.SearchTerm != "Taylor Swift" { t.Errorf("Expected searchTerm 'Taylor Swift', got %s", request.SearchTerm) } @@ -829,8 +851,8 @@ func TestClient_SearchPandoraStations(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.SearchPandoraStations("user123", "Taylor Swift") + response, err := client.SearchPandoraStations("user123", "Taylor Swift") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -872,8 +894,8 @@ func TestClient_SearchTuneInStations(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.SearchTuneInStations("Jazz") + response, err := client.SearchTuneInStations("Jazz") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -914,8 +936,8 @@ func TestClient_SearchSpotifyContent(t *testing.T) { } client := NewClient(config) client.baseURL = server.URL - response, err := client.SearchSpotifyContent("user@example.com", "Queen") + response, err := client.SearchSpotifyContent("user@example.com", "Queen") if err != nil { t.Errorf("Unexpected error: %v", err) return diff --git a/pkg/client/navigation_xml_test.go b/pkg/client/navigation_xml_test.go index b53925c..b086f27 100644 --- a/pkg/client/navigation_xml_test.go +++ b/pkg/client/navigation_xml_test.go @@ -50,8 +50,10 @@ func TestClient_NavigateXMLValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var capturedXML string - var capturedEndpoint string + var ( + capturedXML string + capturedEndpoint string + ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedEndpoint = r.URL.Path @@ -277,9 +279,11 @@ func TestClient_RemoveStationXMLValidation(t *testing.T) { if !strings.Contains(capturedXML, `source="PANDORA"`) { t.Error("XML should contain source attribute") } + if !strings.Contains(capturedXML, `location="126740707481236361"`) { t.Error("XML should contain location attribute") } + if !strings.Contains(capturedXML, `Test Station`) { t.Error("XML should contain itemName element") } @@ -373,6 +377,7 @@ func TestClient_NavigationResponseParsing(t *testing.T) { if err == nil { t.Error("Expected error but got none") } + return } @@ -410,9 +415,11 @@ func TestClient_NavigationResponseParsing(t *testing.T) { if !firstItem.IsPlayable() { t.Error("First item should be playable") } + if !firstItem.IsDirectory() { t.Error("First item should be directory") } + if firstItem.GetArtwork() == "" { t.Error("First item should have artwork") } @@ -421,6 +428,7 @@ func TestClient_NavigationResponseParsing(t *testing.T) { if !secondItem.IsTrack() { t.Error("Second item should be track") } + if secondItem.ArtistName != "Test Artist" { t.Errorf("Expected artist 'Test Artist', got %s", secondItem.ArtistName) } @@ -488,6 +496,7 @@ func TestClient_SearchStationResponseParsing(t *testing.T) { if response.DeviceID != "1004567890AA" { t.Errorf("Expected deviceID '1004567890AA', got %s", response.DeviceID) } + if response.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", response.Source) } @@ -518,6 +527,7 @@ func TestClient_SearchStationResponseParsing(t *testing.T) { if !song.IsSong() { t.Error("First result should be identified as song") } + if song.GetFullTitle() != "Old Church Choir - Zach Williams" { t.Errorf("Expected 'Old Church Choir - Zach Williams', got %s", song.GetFullTitle()) } @@ -526,6 +536,7 @@ func TestClient_SearchStationResponseParsing(t *testing.T) { if !artist.IsArtist() { t.Error("Artist result should be identified as artist") } + if artist.GetDisplayName() != "Zach Williams" { t.Errorf("Expected 'Zach Williams', got %s", artist.GetDisplayName()) } @@ -534,6 +545,7 @@ func TestClient_SearchStationResponseParsing(t *testing.T) { if !station.IsStation() { t.Error("Station result should be identified as station") } + if station.Description == "" { t.Error("Station should have description") } diff --git a/pkg/client/preset_test.go b/pkg/client/preset_test.go index bdd7399..91e11e8 100644 --- a/pkg/client/preset_test.go +++ b/pkg/client/preset_test.go @@ -103,6 +103,7 @@ func TestClient_StorePreset(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("Expected POST request, got %s", r.Method) } + if r.URL.Path != "/storePreset" { t.Errorf("Expected /storePreset endpoint, got %s", r.URL.Path) } @@ -116,6 +117,7 @@ func TestClient_StorePreset(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -134,6 +136,7 @@ func TestClient_StorePreset(t *testing.T) { t.Errorf("Expected error, but got nil") return } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) } @@ -191,6 +194,7 @@ func TestClient_RemovePreset(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("Expected POST request, got %s", r.Method) } + if r.URL.Path != "/removePreset" { t.Errorf("Expected /removePreset endpoint, got %s", r.URL.Path) } @@ -199,6 +203,7 @@ func TestClient_RemovePreset(t *testing.T) { if tt.serverStatus != 0 { w.WriteHeader(tt.serverStatus) } + if tt.serverResponse != "" { _, _ = w.Write([]byte(tt.serverResponse)) } @@ -217,6 +222,7 @@ func TestClient_RemovePreset(t *testing.T) { t.Errorf("Expected error, but got nil") return } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) } @@ -342,6 +348,7 @@ func TestClient_StoreCurrentAsPreset(t *testing.T) { } else { w.WriteHeader(http.StatusOK) } + if tt.nowPlayingResponse != "" { _, _ = w.Write([]byte(tt.nowPlayingResponse)) } @@ -351,6 +358,7 @@ func TestClient_StoreCurrentAsPreset(t *testing.T) { } else { w.WriteHeader(http.StatusOK) } + _, _ = w.Write([]byte(``)) default: w.WriteHeader(http.StatusNotFound) @@ -370,6 +378,7 @@ func TestClient_StoreCurrentAsPreset(t *testing.T) { t.Errorf("Expected error, but got nil") return } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) } @@ -384,6 +393,7 @@ func TestClient_StoreCurrentAsPreset(t *testing.T) { func TestClient_StorePreset_XMLGeneration(t *testing.T) { var capturedXML string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := make([]byte, r.ContentLength) _, _ = r.Body.Read(body) @@ -435,6 +445,7 @@ func TestClient_StorePreset_XMLGeneration(t *testing.T) { func TestClient_RemovePreset_XMLGeneration(t *testing.T) { var capturedXML string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := make([]byte, r.ContentLength) _, _ = r.Body.Read(body) diff --git a/pkg/client/serviceavailability_integration_test.go b/pkg/client/serviceavailability_integration_test.go index fe07862..e1cc3b2 100644 --- a/pkg/client/serviceavailability_integration_test.go +++ b/pkg/client/serviceavailability_integration_test.go @@ -50,6 +50,7 @@ func TestGetServiceAvailability_Integration(t *testing.T) { status += " (" + service.Reason + ")" } } + t.Logf("Service %s: %s", service.Type, status) } } @@ -66,6 +67,7 @@ func TestGetServiceAvailability_Integration(t *testing.T) { // 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) } @@ -181,6 +183,7 @@ func TestGetServiceAvailability_UserFeedback(t *testing.T) { availableServices := serviceAvailability.GetAvailableServices() if len(availableServices) > 0 { t.Log("\nAvailable Services:") + for _, service := range availableServices { t.Logf(" āœ… %s", formatServiceName(service.Type)) } @@ -189,11 +192,13 @@ func TestGetServiceAvailability_UserFeedback(t *testing.T) { 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) } } @@ -201,21 +206,25 @@ func TestGetServiceAvailability_UserFeedback(t *testing.T) { // 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 ===") diff --git a/pkg/client/serviceavailability_test.go b/pkg/client/serviceavailability_test.go index 054a8af..7b8bde8 100644 --- a/pkg/client/serviceavailability_test.go +++ b/pkg/client/serviceavailability_test.go @@ -41,9 +41,11 @@ func TestGetServiceAvailability(t *testing.T) { 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") } @@ -67,21 +69,27 @@ func TestGetServiceAvailability(t *testing.T) { 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") } @@ -91,9 +99,11 @@ func TestGetServiceAvailability(t *testing.T) { 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()) } @@ -125,9 +135,11 @@ func TestGetServiceAvailability(t *testing.T) { 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") } @@ -135,9 +147,11 @@ func TestGetServiceAvailability(t *testing.T) { 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()) } @@ -145,9 +159,11 @@ func TestGetServiceAvailability(t *testing.T) { if !sa.HasSpotify() { t.Error("should have Spotify") } + if !sa.HasBluetooth() { t.Error("should have Bluetooth") } + if !sa.HasAirPlay() { t.Error("should have AirPlay") } @@ -164,9 +180,11 @@ func TestGetServiceAvailability(t *testing.T) { 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") } @@ -174,9 +192,11 @@ func TestGetServiceAvailability(t *testing.T) { 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()) } @@ -184,6 +204,7 @@ func TestGetServiceAvailability(t *testing.T) { if sa.HasSpotify() { t.Error("should not have Spotify") } + if sa.HasBluetooth() { t.Error("should not have Bluetooth") } @@ -212,6 +233,7 @@ func TestGetServiceAvailability(t *testing.T) { 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) } @@ -232,6 +254,7 @@ func TestGetServiceAvailability(t *testing.T) { if err == nil { t.Error("expected an error but got none") } + if result != nil { t.Error("expected nil result on error") } @@ -239,6 +262,7 @@ func TestGetServiceAvailability(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } + tt.validate(t, result) } }) @@ -250,13 +274,14 @@ func TestGetServiceAvailability_NetworkError(t *testing.T) { 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) } @@ -269,27 +294,35 @@ func TestServiceAvailabilityModel_EdgeCases(t *testing.T) { 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") } @@ -304,6 +337,7 @@ func TestServiceAvailabilityModel_EdgeCases(t *testing.T) { 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") } @@ -324,6 +358,7 @@ func TestServiceAvailabilityModel_EdgeCases(t *testing.T) { 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()) } diff --git a/pkg/client/supported_urls_test.go b/pkg/client/supported_urls_test.go index 0f97383..c9667ed 100644 --- a/pkg/client/supported_urls_test.go +++ b/pkg/client/supported_urls_test.go @@ -96,6 +96,7 @@ func TestClient_GetSupportedURLs(t *testing.T) { 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) } @@ -124,6 +125,7 @@ func TestClient_GetSupportedURLs(t *testing.T) { if tt.expectedError && err == nil { t.Errorf("Expected error, but got none") } + if !tt.expectedError && err != nil { t.Errorf("Unexpected error: %v", err) } @@ -179,6 +181,7 @@ func TestClient_GetSupportedURLs_ServerError(t *testing.T) { 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") } @@ -207,6 +210,7 @@ func TestClient_GetSupportedURLs_NotFound(t *testing.T) { 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") } @@ -236,6 +240,7 @@ func TestClient_GetSupportedURLs_InvalidXML(t *testing.T) { 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") } @@ -268,6 +273,7 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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]) } @@ -277,9 +283,11 @@ func TestSupportedURLsResponse_Methods(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") } @@ -294,18 +302,22 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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) } @@ -314,6 +326,7 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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)) @@ -322,6 +335,7 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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)) @@ -330,6 +344,7 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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)) @@ -369,18 +384,22 @@ func TestSupportedURLsResponse_Methods(t *testing.T) { 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) } @@ -399,12 +418,15 @@ func TestSupportedURLsResponse_EmptyURLs(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") } @@ -536,6 +558,7 @@ func TestSupportedURLsResponse_FeatureMapping(t *testing.T) { // 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) } @@ -602,15 +625,19 @@ func TestEndpointFeatureMap(t *testing.T) { 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) } @@ -633,6 +660,7 @@ func TestEndpointFeatureMap(t *testing.T) { t.Run("EssentialFeatures", func(t *testing.T) { essentialCount := 0 + for _, feature := range features { if feature.Essential { essentialCount++ diff --git a/pkg/models/navigation.go b/pkg/models/navigation.go index 00c27b5..d753b1b 100644 --- a/pkg/models/navigation.go +++ b/pkg/models/navigation.go @@ -268,6 +268,7 @@ func (sr *SearchStationResponse) GetAllResults() []SearchResult { allResults = append(allResults, sr.Songs...) allResults = append(allResults, sr.Artists...) allResults = append(allResults, sr.Stations...) + return allResults } @@ -306,6 +307,7 @@ func (sr *SearchResult) GetDisplayName() string { if sr.Name != "" { return sr.Name } + return "Unknown" } @@ -337,5 +339,6 @@ func (sr *SearchResult) GetFullTitle() string { if sr.Artist != "" { return sr.Name + " - " + sr.Artist } + return sr.Name } diff --git a/pkg/models/navigation_test.go b/pkg/models/navigation_test.go index fc7cc43..a4f76ff 100644 --- a/pkg/models/navigation_test.go +++ b/pkg/models/navigation_test.go @@ -15,9 +15,11 @@ func TestNavigateRequest_NewNavigateRequest(t *testing.T) { if req.SourceAccount != "user@example.com" { t.Errorf("Expected sourceAccount user@example.com, got %s", req.SourceAccount) } + if req.StartItem != 1 { t.Errorf("Expected startItem 1, got %d", req.StartItem) } + if req.NumItems != 50 { t.Errorf("Expected numItems 50, got %d", req.NumItems) } @@ -29,9 +31,11 @@ func TestNavigateRequest_NewNavigateRequestWithMenu(t *testing.T) { if req.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", req.Source) } + if req.Menu != "radioStations" { t.Errorf("Expected menu radioStations, got %s", req.Menu) } + if req.Sort != "dateCreated" { t.Errorf("Expected sort dateCreated, got %s", req.Sort) } @@ -99,6 +103,7 @@ func TestNavigateRequest_XMLMarshalWithItem(t *testing.T) { if !contains(xmlStr, `source="STORED_MUSIC"`) { t.Error("XML should contain source attribute") } + if !contains(xmlStr, `1`) { t.Error("XML should contain startItem element") } @@ -110,6 +115,7 @@ func TestNavigateRequest_XMLMarshalWithItem(t *testing.T) { if !contains(xmlStr, `Test Station`) { t.Error("XML should contain itemName element") } @@ -402,9 +419,11 @@ func TestSearchStationRequest_NewSearchStationRequest(t *testing.T) { if req.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", req.Source) } + if req.SourceAccount != "user123" { t.Errorf("Expected sourceAccount user123, got %s", req.SourceAccount) } + if req.SearchTerm != "Zach Williams" { t.Errorf("Expected searchTerm 'Zach Williams', got %s", req.SearchTerm) } @@ -483,15 +502,19 @@ func TestSearchStationResponse_XMLUnmarshal(t *testing.T) { if response.DeviceID != "1004567890AA" { t.Errorf("Expected deviceID '1004567890AA', got %s", response.DeviceID) } + if response.Source != "PANDORA" { t.Errorf("Expected source PANDORA, got %s", response.Source) } + if len(response.Songs) != 1 { t.Errorf("Expected 1 song result, got %d", len(response.Songs)) } + if len(response.Artists) != 1 { t.Errorf("Expected 1 artist result, got %d", len(response.Artists)) } + if len(response.Stations) != 1 { t.Errorf("Expected 1 station result, got %d", len(response.Stations)) } @@ -501,9 +524,11 @@ func TestSearchStationResponse_XMLUnmarshal(t *testing.T) { if song.Name != "Old Church Choir" { t.Errorf("Expected song name 'Old Church Choir', got %s", song.Name) } + if song.Artist != "Zach Williams" { t.Errorf("Expected artist 'Zach Williams', got %s", song.Artist) } + if song.Token != "S10657777" { t.Errorf("Expected token 'S10657777', got %s", song.Token) } @@ -513,6 +538,7 @@ func TestSearchStationResponse_XMLUnmarshal(t *testing.T) { if artist.Name != "Zach Williams" { t.Errorf("Expected artist name 'Zach Williams', got %s", artist.Name) } + if !artist.IsArtist() { t.Error("Expected result to be identified as artist") } @@ -522,6 +548,7 @@ func TestSearchStationResponse_XMLUnmarshal(t *testing.T) { if station.Name != "Classic Rock Station" { t.Errorf("Expected station name 'Classic Rock Station', got %s", station.Name) } + if !station.IsStation() { t.Error("Expected result to be identified as station") } @@ -585,6 +612,7 @@ func TestSearchStationResponse_HelperMethods(t *testing.T) { if !emptyResponse.IsEmpty() { t.Error("Expected empty response to be empty") } + if emptyResponse.HasResults() { t.Error("Expected empty response to have no results") } @@ -630,6 +658,7 @@ func TestSearchResult_HelperMethods(t *testing.T) { if !tt.result.IsSong() { t.Error("Expected result to be identified as song") } + if tt.result.IsArtist() || tt.result.IsStation() { t.Error("Result incorrectly identified as artist or station") } @@ -637,6 +666,7 @@ func TestSearchResult_HelperMethods(t *testing.T) { if !tt.result.IsArtist() { t.Error("Expected result to be identified as artist") } + if tt.result.IsSong() || tt.result.IsStation() { t.Error("Result incorrectly identified as song or station") } @@ -644,6 +674,7 @@ func TestSearchResult_HelperMethods(t *testing.T) { if !tt.result.IsStation() { t.Error("Expected result to be identified as station") } + if tt.result.IsSong() || tt.result.IsArtist() { t.Error("Result incorrectly identified as song or artist") } @@ -685,5 +716,6 @@ func containsSubstring(s, substr string) bool { return true } } + return false } diff --git a/pkg/models/serviceavailability.go b/pkg/models/serviceavailability.go index c1df534..37bff4d 100644 --- a/pkg/models/serviceavailability.go +++ b/pkg/models/serviceavailability.go @@ -75,6 +75,7 @@ func (sa *ServiceAvailability) GetAvailableServices() []Service { available = append(available, service) } } + return available } @@ -85,11 +86,13 @@ func (sa *ServiceAvailability) GetUnavailableServices() []Service { } var unavailable []Service + for _, service := range sa.Services.Service { if !service.IsAvailable { unavailable = append(unavailable, service) } } + return unavailable } @@ -104,6 +107,7 @@ func (sa *ServiceAvailability) IsServiceAvailable(serviceType ServiceType) bool return true } } + return false } @@ -118,6 +122,7 @@ func (sa *ServiceAvailability) GetServiceByType(serviceType ServiceType) *Servic return &service } } + return nil } @@ -182,6 +187,7 @@ func (sa *ServiceAvailability) GetStreamingServices() []Service { } } } + return streaming } @@ -198,6 +204,7 @@ func (sa *ServiceAvailability) GetLocalServices() []Service { } var local []Service + for _, service := range sa.Services.Service { for _, localType := range localTypes { if service.Type == string(localType) { @@ -206,6 +213,7 @@ func (sa *ServiceAvailability) GetLocalServices() []Service { } } } + return local } @@ -214,6 +222,7 @@ func (sa *ServiceAvailability) GetServiceCount() int { if sa.Services == nil { return 0 } + return len(sa.Services.Service) } diff --git a/pkg/models/serviceavailability_test.go b/pkg/models/serviceavailability_test.go index 185c904..178b908 100644 --- a/pkg/models/serviceavailability_test.go +++ b/pkg/models/serviceavailability_test.go @@ -33,9 +33,11 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { `, validate: func(t *testing.T, sa *ServiceAvailability) { t.Helper() + if sa.Services == nil { t.Fatal("services should not be nil") } + if len(sa.Services.Service) != 13 { t.Errorf("expected 13 services, got %d", len(sa.Services.Service)) } @@ -45,9 +47,11 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { if spotifyService == nil { t.Fatal("spotify service should not be nil") } + if !spotifyService.IsAvailable { t.Error("spotify service should be available") } + if spotifyService.Reason != "" { t.Error("spotify service should not have a reason") } @@ -56,9 +60,11 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { if bluetoothService == nil { t.Fatal("bluetooth service should not be nil") } + if bluetoothService.IsAvailable { t.Error("bluetooth service should not be available") } + if bluetoothService.Reason != "INVALID_SOURCE_TYPE" { t.Errorf("bluetooth service reason should be 'INVALID_SOURCE_TYPE', got '%s'", bluetoothService.Reason) } @@ -73,9 +79,11 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { `, validate: func(t *testing.T, sa *ServiceAvailability) { t.Helper() + if sa.Services == nil { t.Fatal("services should not be nil") } + if len(sa.Services.Service) != 0 { t.Errorf("expected 0 services, got %d", len(sa.Services.Service)) } @@ -90,15 +98,19 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { `, validate: func(t *testing.T, sa *ServiceAvailability) { t.Helper() + if sa.Services == nil { t.Fatal("services should not be nil") } + if len(sa.Services.Service) != 1 { t.Errorf("expected 1 service, got %d", len(sa.Services.Service)) } + if sa.Services.Service[0].Type != "SPOTIFY" { t.Errorf("expected SPOTIFY, got %s", sa.Services.Service[0].Type) } + if !sa.Services.Service[0].IsAvailable { t.Error("service should be available") } @@ -109,6 +121,7 @@ func TestServiceAvailability_UnmarshalXML(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var sa ServiceAvailability + err := xml.Unmarshal([]byte(tt.xmlData), &sa) if err != nil { t.Fatalf("failed to unmarshal XML: %v", err) @@ -135,9 +148,11 @@ func TestServiceAvailability_GetAvailableServices(t *testing.T) { if len(available) != 2 { t.Errorf("expected 2 available services, got %d", len(available)) } + if available[0].Type != "SPOTIFY" { t.Errorf("expected first service to be SPOTIFY, got %s", available[0].Type) } + if available[1].Type != "AIRPLAY" { t.Errorf("expected second service to be AIRPLAY, got %s", available[1].Type) } @@ -159,9 +174,11 @@ func TestServiceAvailability_GetUnavailableServices(t *testing.T) { if len(unavailable) != 2 { t.Errorf("expected 2 unavailable services, got %d", len(unavailable)) } + if unavailable[0].Type != "BLUETOOTH" { t.Errorf("expected first service to be BLUETOOTH, got %s", unavailable[0].Type) } + if unavailable[1].Type != "ALEXA" { t.Errorf("expected second service to be ALEXA, got %s", unavailable[1].Type) } @@ -180,9 +197,11 @@ func TestServiceAvailability_IsServiceAvailable(t *testing.T) { if !sa.IsServiceAvailable(ServiceTypeSpotify) { t.Error("Spotify should be available") } + if sa.IsServiceAvailable(ServiceTypeBluetooth) { t.Error("Bluetooth should not be available") } + if sa.IsServiceAvailable(ServiceTypeAlexa) { t.Error("Alexa should not be available (not in list)") } @@ -202,9 +221,11 @@ func TestServiceAvailability_GetServiceByType(t *testing.T) { if spotifyService == nil { t.Fatal("spotify service should not be nil") } + if spotifyService.Type != "SPOTIFY" { t.Errorf("expected SPOTIFY, got %s", spotifyService.Type) } + if !spotifyService.IsAvailable { t.Error("spotify service should be available") } @@ -213,12 +234,15 @@ func TestServiceAvailability_GetServiceByType(t *testing.T) { if bluetoothService == nil { t.Fatal("bluetooth service should not be nil") } + if bluetoothService.Type != "BLUETOOTH" { t.Errorf("expected BLUETOOTH, got %s", bluetoothService.Type) } + if bluetoothService.IsAvailable { t.Error("bluetooth service should not be available") } + if bluetoothService.Reason != "DEVICE_NOT_FOUND" { t.Errorf("expected DEVICE_NOT_FOUND, got %s", bluetoothService.Reason) } @@ -247,21 +271,27 @@ func TestServiceAvailability_ConvenienceMethods(t *testing.T) { if !sa.HasSpotify() { t.Error("should have Spotify") } + if sa.HasBluetooth() { t.Error("should not have Bluetooth") } + if !sa.HasAirPlay() { t.Error("should have AirPlay") } + if sa.HasAlexa() { t.Error("should not have Alexa") } + 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") } @@ -362,9 +392,11 @@ func TestServiceAvailability_CountMethods(t *testing.T) { if sa.GetServiceCount() != 4 { t.Errorf("expected 4 total services, got %d", sa.GetServiceCount()) } + if sa.GetAvailableServiceCount() != 2 { t.Errorf("expected 2 available services, got %d", sa.GetAvailableServiceCount()) } + if sa.GetUnavailableServiceCount() != 2 { t.Errorf("expected 2 unavailable services, got %d", sa.GetUnavailableServiceCount()) } @@ -376,33 +408,43 @@ func TestServiceAvailability_NilServicesHandling(t *testing.T) { 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 sa.IsServiceAvailable(ServiceTypeSpotify) { t.Error("Spotify should not be available") } + if sa.GetServiceByType(ServiceTypeSpotify) != nil { t.Error("service should be nil") } + if sa.HasSpotify() { t.Error("should not have Spotify") } + if sa.HasBluetooth() { t.Error("should not have Bluetooth") } + if len(sa.GetStreamingServices()) != 0 { t.Error("streaming services should be empty") } + if len(sa.GetLocalServices()) != 0 { t.Error("local services should be empty") } + if sa.GetServiceCount() != 0 { t.Error("service count should be 0") } + if sa.GetAvailableServiceCount() != 0 { t.Error("available service count should be 0") } + if sa.GetUnavailableServiceCount() != 0 { t.Error("unavailable service count should be 0") } @@ -414,6 +456,7 @@ func TestService_Methods(t *testing.T) { if !service.IsType(ServiceTypeSpotify) { t.Error("service should be of type Spotify") } + if service.IsType(ServiceTypeBluetooth) { t.Error("service should not be of type Bluetooth") } @@ -441,39 +484,51 @@ func TestServiceType_Constants(t *testing.T) { if ServiceTypeAirPlay != ServiceType("AIRPLAY") { t.Error("ServiceTypeAirPlay constant mismatch") } + if ServiceTypeAlexa != ServiceType("ALEXA") { t.Error("ServiceTypeAlexa constant mismatch") } + if ServiceTypeAmazon != ServiceType("AMAZON") { t.Error("ServiceTypeAmazon constant mismatch") } + if ServiceTypeBluetooth != ServiceType("BLUETOOTH") { t.Error("ServiceTypeBluetooth constant mismatch") } + if ServiceTypeBMX != ServiceType("BMX") { t.Error("ServiceTypeBMX constant mismatch") } + if ServiceTypeDeezer != ServiceType("DEEZER") { t.Error("ServiceTypeDeezer constant mismatch") } + if ServiceTypeIHeart != ServiceType("IHEART") { t.Error("ServiceTypeIHeart constant mismatch") } + if ServiceTypeLocalInternetRadio != ServiceType("LOCAL_INTERNET_RADIO") { t.Error("ServiceTypeLocalInternetRadio constant mismatch") } + if ServiceTypeLocalMusic != ServiceType("LOCAL_MUSIC") { t.Error("ServiceTypeLocalMusic constant mismatch") } + if ServiceTypeNotification != ServiceType("NOTIFICATION") { t.Error("ServiceTypeNotification constant mismatch") } + if ServiceTypePandora != ServiceType("PANDORA") { t.Error("ServiceTypePandora constant mismatch") } + if ServiceTypeSpotify != ServiceType("SPOTIFY") { t.Error("ServiceTypeSpotify constant mismatch") } + if ServiceTypeTuneIn != ServiceType("TUNEIN") { t.Error("ServiceTypeTuneIn constant mismatch") } @@ -496,6 +551,7 @@ func TestServiceAvailability_MarshalXML(t *testing.T) { // Unmarshal back to verify roundtrip var unmarshaled ServiceAvailability + err = xml.Unmarshal(data, &unmarshaled) if err != nil { t.Fatalf("failed to unmarshal XML: %v", err) @@ -504,9 +560,11 @@ func TestServiceAvailability_MarshalXML(t *testing.T) { if sa.GetServiceCount() != unmarshaled.GetServiceCount() { t.Error("service count mismatch after roundtrip") } + if sa.HasSpotify() != unmarshaled.HasSpotify() { t.Error("Spotify availability mismatch after roundtrip") } + if sa.HasBluetooth() != unmarshaled.HasBluetooth() { t.Error("Bluetooth availability mismatch after roundtrip") } @@ -515,6 +573,7 @@ func TestServiceAvailability_MarshalXML(t *testing.T) { if bluetoothService == nil { t.Fatal("bluetooth service should not be nil after roundtrip") } + if bluetoothService.Reason != "UNAVAILABLE" { t.Errorf("expected UNAVAILABLE reason, got %s", bluetoothService.Reason) } diff --git a/pkg/models/supportedurls.go b/pkg/models/supportedurls.go index 4200b0a..b95bf8e 100644 --- a/pkg/models/supportedurls.go +++ b/pkg/models/supportedurls.go @@ -20,6 +20,7 @@ func (s *SupportedURLsResponse) GetURLs() []string { for i, url := range s.URLs { urls[i] = url.Location } + return urls } @@ -30,6 +31,7 @@ func (s *SupportedURLsResponse) HasURL(location string) bool { return true } } + return false } @@ -47,11 +49,13 @@ func (s *SupportedURLsResponse) GetCoreURLs() []string { } var available []string + for _, endpoint := range coreEndpoints { if s.HasURL(endpoint) { available = append(available, endpoint) } } + return available } @@ -63,11 +67,13 @@ func (s *SupportedURLsResponse) GetStreamingURLs() []string { } var available []string + for _, endpoint := range streamingEndpoints { if s.HasURL(endpoint) { available = append(available, endpoint) } } + return available } @@ -81,11 +87,13 @@ func (s *SupportedURLsResponse) GetAdvancedURLs() []string { } var available []string + for _, endpoint := range advancedEndpoints { if s.HasURL(endpoint) { available = append(available, endpoint) } } + return available } @@ -97,11 +105,13 @@ func (s *SupportedURLsResponse) GetNetworkURLs() []string { } var available []string + for _, endpoint := range networkEndpoints { if s.HasURL(endpoint) { available = append(available, endpoint) } } + return available } @@ -113,6 +123,7 @@ func (s *SupportedURLsResponse) HasCorePlaybackSupport() bool { return false } } + return true } @@ -139,11 +150,13 @@ func (s *SupportedURLsResponse) HasStreamingSupport() bool { // GetUnsupportedURLs returns a list of common URLs that this device doesn't support func (s *SupportedURLsResponse) GetUnsupportedURLs(checkList []string) []string { var unsupported []string + for _, endpoint := range checkList { if !s.HasURL(endpoint) { unsupported = append(unsupported, endpoint) } } + return unsupported } @@ -338,6 +351,7 @@ func (s *SupportedURLsResponse) GetFeaturesByCategory() map[string][]EndpointFea for _, feature := range features { // Check if device supports this feature (any of its endpoints) supported := false + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { supported = true @@ -362,6 +376,7 @@ func (s *SupportedURLsResponse) GetSupportedFeatures() []EndpointFeature { for _, feature := range features { // Check if device supports this feature (any of its endpoints) hasSupport := false + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { hasSupport = true @@ -386,6 +401,7 @@ func (s *SupportedURLsResponse) GetUnsupportedFeatures() []EndpointFeature { for _, feature := range features { // Check if device supports this feature (any of its endpoints) hasSupport := false + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { hasSupport = true @@ -413,6 +429,7 @@ func (s *SupportedURLsResponse) GetPartiallyImplementedFeatures() []EndpointFeat } supportedCount := 0 + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { supportedCount++ @@ -441,6 +458,7 @@ func (s *SupportedURLsResponse) GetMissingEssentialFeatures() []EndpointFeature // Check if device supports this essential feature hasSupport := false + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { hasSupport = true @@ -471,6 +489,7 @@ func (s *SupportedURLsResponse) GetFeatureCompleteness() (int, int, int) { // Check if device supports this feature hasSupport := false + for _, endpoint := range feature.Endpoints { if s.HasURL(endpoint) { hasSupport = true