package models import ( "encoding/xml" "testing" ) func TestRecentsResponse_Unmarshal(t *testing.T) { tests := []struct { name string xmlData string expected *RecentsResponse expectError bool }{ { name: "complete recents response", xmlData: ` MercyMe, It's Christmas! Baby It's Cold Outside - ANNE MURRAY `, expected: &RecentsResponse{ Items: []RecentsResponseItem{ { DeviceID: "1004567890AA", UTCTime: 1701202831, ContentItem: &ContentItem{ Source: "STORED_MUSIC", Location: "6_a2874b5d_4f83d999", SourceAccount: "d09708a1-5953-44bc-a413-123456789012/0", IsPresetable: true, ItemName: "MercyMe, It's Christmas!", }, }, { DeviceID: "1004567890AA", UTCTime: 1700232917, ID: "2487503626", ContentItem: &ContentItem{ Source: "LOCAL_MUSIC", Type: "track", Location: "track:2590", SourceAccount: "3f205110-4a57-4e91-810a-123456789012", IsPresetable: true, ItemName: "Baby It's Cold Outside - ANNE MURRAY", }, }, }, }, }, { name: "spotify recent item", xmlData: ` Shape of You - Ed Sheeran https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96 `, expected: &RecentsResponse{ Items: []RecentsResponseItem{ { DeviceID: "1004567890AA", UTCTime: 1701300000, ID: "spotify123", ContentItem: &ContentItem{ Source: "SPOTIFY", Type: "track", Location: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh", SourceAccount: "spotify_user", IsPresetable: true, ItemName: "Shape of You - Ed Sheeran", ContainerArt: "https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96", }, }, }, }, }, { name: "empty recents", xmlData: ` `, expected: &RecentsResponse{ Items: []RecentsResponseItem{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var response RecentsResponse err := xml.Unmarshal([]byte(tt.xmlData), &response) if tt.expectError { if err == nil { t.Error("expected error, got nil") } return } if err != nil { t.Fatalf("failed to unmarshal response: %v", err) } // Compare basic structure if len(response.Items) != len(tt.expected.Items) { t.Errorf("expected %d items, got %d", len(tt.expected.Items), len(response.Items)) } // Compare each item for i, expectedItem := range tt.expected.Items { if i >= len(response.Items) { break } actualItem := response.Items[i] if actualItem.DeviceID != expectedItem.DeviceID { t.Errorf("item %d: expected deviceID %s, got %s", i, expectedItem.DeviceID, actualItem.DeviceID) } if actualItem.UTCTime != expectedItem.UTCTime { t.Errorf("item %d: expected utcTime %d, got %d", i, expectedItem.UTCTime, actualItem.UTCTime) } if actualItem.ID != expectedItem.ID { t.Errorf("item %d: expected id %s, got %s", i, expectedItem.ID, actualItem.ID) } // Compare ContentItem if expectedItem.ContentItem != nil { if actualItem.ContentItem == nil { t.Errorf("item %d: expected contentItem, got nil", i) continue } if actualItem.ContentItem.Source != expectedItem.ContentItem.Source { t.Errorf("item %d: expected source %s, got %s", i, expectedItem.ContentItem.Source, actualItem.ContentItem.Source) } if actualItem.ContentItem.Type != expectedItem.ContentItem.Type { t.Errorf("item %d: expected type %s, got %s", i, expectedItem.ContentItem.Type, actualItem.ContentItem.Type) } if actualItem.ContentItem.Location != expectedItem.ContentItem.Location { t.Errorf("item %d: expected location %s, got %s", i, expectedItem.ContentItem.Location, actualItem.ContentItem.Location) } if actualItem.ContentItem.ItemName != expectedItem.ContentItem.ItemName { t.Errorf("item %d: expected itemName %s, got %s", i, expectedItem.ContentItem.ItemName, actualItem.ContentItem.ItemName) } if actualItem.ContentItem.IsPresetable != expectedItem.ContentItem.IsPresetable { t.Errorf("item %d: expected isPresetable %t, got %t", i, expectedItem.ContentItem.IsPresetable, actualItem.ContentItem.IsPresetable) } } else if actualItem.ContentItem != nil { t.Errorf("item %d: expected nil contentItem, got non-nil", i) } } }) } } func TestRecentsResponse_Methods(t *testing.T) { response := &RecentsResponse{ Items: []RecentsResponseItem{ { DeviceID: "device1", UTCTime: 1701200000, ContentItem: &ContentItem{ Source: "SPOTIFY", Type: "track", ItemName: "Song 1", IsPresetable: true, }, }, { DeviceID: "device1", UTCTime: 1701100000, ContentItem: &ContentItem{ Source: "LOCAL_MUSIC", Type: "track", ItemName: "Song 2", IsPresetable: false, }, }, { DeviceID: "device1", UTCTime: 1701000000, ContentItem: &ContentItem{ Source: "TUNEIN", Type: "stationurl", ItemName: "Radio Station", IsPresetable: true, }, }, }, } // Test GetItemCount if response.GetItemCount() != 3 { t.Errorf("expected item count 3, got %d", response.GetItemCount()) } // Test IsEmpty if response.IsEmpty() { t.Error("expected IsEmpty() to return false") } // Test GetMostRecent mostRecent := response.GetMostRecent() if mostRecent == nil { t.Error("expected most recent item, got nil") } else if mostRecent.UTCTime != 1701200000 { t.Errorf("expected most recent UTCTime 1701200000, got %d", mostRecent.UTCTime) } // Test GetItemsBySource spotifyItems := response.GetItemsBySource("SPOTIFY") if len(spotifyItems) != 1 { t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems)) } localItems := response.GetItemsBySource("LOCAL_MUSIC") if len(localItems) != 1 { t.Errorf("expected 1 LOCAL_MUSIC item, got %d", len(localItems)) } // Test GetSpotifyItems spotifyItems2 := response.GetSpotifyItems() if len(spotifyItems2) != 1 { t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems2)) } // Test GetPresetableItems presetableItems := response.GetPresetableItems() if len(presetableItems) != 2 { t.Errorf("expected 2 presetable items, got %d", len(presetableItems)) } // Test GetTracks tracks := response.GetTracks() if len(tracks) != 2 { t.Errorf("expected 2 track items, got %d", len(tracks)) } // Test GetStations stations := response.GetStations() if len(stations) != 1 { t.Errorf("expected 1 station item, got %d", len(stations)) } } func TestRecentsResponse_EmptyResponse(t *testing.T) { response := &RecentsResponse{ Items: []RecentsResponseItem{}, } // Test empty response methods if response.GetItemCount() != 0 { t.Errorf("expected item count 0, got %d", response.GetItemCount()) } if !response.IsEmpty() { t.Error("expected IsEmpty() to return true") } if response.GetMostRecent() != nil { t.Error("expected GetMostRecent() to return nil") } if len(response.GetSpotifyItems()) != 0 { t.Errorf("expected 0 Spotify items, got %d", len(response.GetSpotifyItems())) } } func TestRecentItem_Methods(t *testing.T) { tests := []struct { name string item RecentsResponseItem test func(t *testing.T, item *RecentsResponseItem) }{ { name: "spotify track item", item: RecentsResponseItem{ DeviceID: "device1", UTCTime: 1701200000, ID: "spotify123", ContentItem: &ContentItem{ Source: "SPOTIFY", Type: "track", Location: "spotify:track:123", SourceAccount: "user@spotify.com", IsPresetable: true, ItemName: "Test Song", ContainerArt: "https://example.com/art.jpg", }, }, test: func(t *testing.T, item *RecentsResponseItem) { t.Helper() if !item.HasContent() { t.Error("expected HasContent() to return true") } if item.GetDisplayName() != "Test Song" { t.Errorf("expected display name 'Test Song', got %s", item.GetDisplayName()) } if item.GetSource() != "SPOTIFY" { t.Errorf("expected source 'SPOTIFY', got %s", item.GetSource()) } if !item.IsTrack() { t.Error("expected IsTrack() to return true") } if !item.IsSpotifyContent() { t.Error("expected IsSpotifyContent() to return true") } if !item.IsStreamingContent() { t.Error("expected IsStreamingContent() to return true") } if item.IsLocalContent() { t.Error("expected IsLocalContent() to return false") } if !item.IsPresetable() { t.Error("expected IsPresetable() to return true") } if !item.HasArtwork() { t.Error("expected HasArtwork() to return true") } if item.GetArtwork() != "https://example.com/art.jpg" { t.Errorf("expected artwork URL, got %s", item.GetArtwork()) } if item.GetUTCTime() != 1701200000 { t.Errorf("expected UTC time 1701200000, got %d", item.GetUTCTime()) } if !item.HasID() { t.Error("expected HasID() to return true") } if item.GetID() != "spotify123" { t.Errorf("expected ID 'spotify123', got %s", item.GetID()) } }, }, { name: "local music item", item: RecentsResponseItem{ DeviceID: "device1", UTCTime: 1701100000, ContentItem: &ContentItem{ Source: "LOCAL_MUSIC", Type: "track", Location: "/music/song.mp3", IsPresetable: false, ItemName: "Local Song", }, }, test: func(t *testing.T, item *RecentsResponseItem) { t.Helper() if !item.IsLocalContent() { t.Error("expected IsLocalContent() to return true") } if item.IsStreamingContent() { t.Error("expected IsStreamingContent() to return false") } if item.IsSpotifyContent() { t.Error("expected IsSpotifyContent() to return false") } if item.HasArtwork() { t.Error("expected HasArtwork() to return false") } if item.GetArtwork() != "" { t.Errorf("expected empty artwork, got %s", item.GetArtwork()) } }, }, { name: "radio station item", item: RecentsResponseItem{ DeviceID: "device1", UTCTime: 1701000000, ContentItem: &ContentItem{ Source: "TUNEIN", Type: "stationurl", Location: "tunein:station:123", IsPresetable: true, ItemName: "Rock FM", }, }, test: func(t *testing.T, item *RecentsResponseItem) { t.Helper() if !item.IsStation() { t.Error("expected IsStation() to return true") } if item.IsTrack() { t.Error("expected IsTrack() to return false") } if !item.IsStreamingContent() { t.Error("expected IsStreamingContent() to return true") } }, }, { name: "empty content item", item: RecentsResponseItem{ DeviceID: "device1", UTCTime: 1701000000, }, test: func(t *testing.T, item *RecentsResponseItem) { t.Helper() if item.HasContent() { t.Error("expected HasContent() to return false") } if item.GetDisplayName() != "Unknown Item" { t.Errorf("expected display name 'Unknown Item', got %s", item.GetDisplayName()) } if item.GetSource() != "" { t.Errorf("expected empty source, got %s", item.GetSource()) } if item.IsTrack() { t.Error("expected IsTrack() to return false") } if item.IsPresetable() { t.Error("expected IsPresetable() to return false") } if item.HasID() { t.Error("expected HasID() to return false") } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.test(t, &tt.item) }) } } func TestRecentItem_ContentTypes(t *testing.T) { tests := []struct { name string contentType string expected map[string]bool }{ { name: "track type", contentType: "track", expected: map[string]bool{ "IsTrack": true, "IsStation": false, "IsPlaylist": false, "IsAlbum": false, "IsContainer": false, }, }, { name: "station type", contentType: "stationurl", expected: map[string]bool{ "IsTrack": false, "IsStation": true, "IsPlaylist": false, "IsAlbum": false, "IsContainer": false, }, }, { name: "playlist type", contentType: "playlist", expected: map[string]bool{ "IsTrack": false, "IsStation": false, "IsPlaylist": true, "IsAlbum": false, "IsContainer": false, }, }, { name: "album type", contentType: "album", expected: map[string]bool{ "IsTrack": false, "IsStation": false, "IsPlaylist": false, "IsAlbum": true, "IsContainer": false, }, }, { name: "container type", contentType: "container", expected: map[string]bool{ "IsTrack": false, "IsStation": false, "IsPlaylist": false, "IsAlbum": false, "IsContainer": true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { item := RecentsResponseItem{ ContentItem: &ContentItem{ Type: tt.contentType, }, } results := map[string]bool{ "IsTrack": item.IsTrack(), "IsStation": item.IsStation(), "IsPlaylist": item.IsPlaylist(), "IsAlbum": item.IsAlbum(), "IsContainer": item.IsContainer(), } for method, expected := range tt.expected { if results[method] != expected { t.Errorf("expected %s() to return %t, got %t", method, expected, results[method]) } } }) } } func TestRecentsResponse_FilterMethods(t *testing.T) { response := &RecentsResponse{ Items: []RecentsResponseItem{ { ContentItem: &ContentItem{Source: "SPOTIFY", Type: "track"}, }, { ContentItem: &ContentItem{Source: "PANDORA", Type: "track"}, }, { ContentItem: &ContentItem{Source: "LOCAL_MUSIC", Type: "track"}, }, { ContentItem: &ContentItem{Source: "STORED_MUSIC", Type: "track"}, }, { ContentItem: &ContentItem{Source: "TUNEIN", Type: "stationurl"}, }, { ContentItem: &ContentItem{Source: "SPOTIFY", Type: "playlist"}, }, }, } // Test individual service filters if len(response.GetSpotifyItems()) != 2 { t.Errorf("expected 2 Spotify items, got %d", len(response.GetSpotifyItems())) } if len(response.GetPandoraItems()) != 1 { t.Errorf("expected 1 Pandora item, got %d", len(response.GetPandoraItems())) } if len(response.GetLocalMusicItems()) != 1 { t.Errorf("expected 1 LOCAL_MUSIC item, got %d", len(response.GetLocalMusicItems())) } if len(response.GetStoredMusicItems()) != 1 { t.Errorf("expected 1 STORED_MUSIC item, got %d", len(response.GetStoredMusicItems())) } if len(response.GetTuneInItems()) != 1 { t.Errorf("expected 1 TuneIn item, got %d", len(response.GetTuneInItems())) } // Test type filters if len(response.GetTracks()) != 4 { t.Errorf("expected 4 track items, got %d", len(response.GetTracks())) } if len(response.GetStations()) != 1 { t.Errorf("expected 1 station item, got %d", len(response.GetStations())) } if len(response.GetPlaylistsAndAlbums()) != 1 { t.Errorf("expected 1 playlist/album item, got %d", len(response.GetPlaylistsAndAlbums())) } }