package models import ( "encoding/xml" "testing" "time" ) func TestPlayStatus_UnmarshalXML(t *testing.T) { tests := []struct { name string xmlInput string expected PlayStatus }{ { name: "playing state", xmlInput: `PLAY_STATE`, expected: PlayStatusPlaying, }, { name: "paused state", xmlInput: `PAUSE_STATE`, expected: PlayStatusPaused, }, { name: "stopped state", xmlInput: `STOP_STATE`, expected: PlayStatusStopped, }, { name: "buffering state", xmlInput: `BUFFERING_STATE`, expected: PlayStatusBuffering, }, { name: "unknown state defaults to stopped", xmlInput: `UNKNOWN_STATE`, expected: PlayStatusStopped, }, { name: "empty state defaults to stopped", xmlInput: ``, expected: PlayStatusStopped, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var status PlayStatus err := xml.Unmarshal([]byte(tt.xmlInput), &status) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } if status != tt.expected { t.Errorf("Expected %s, got %s", tt.expected, status) } }) } } func TestPlayStatus_Methods(t *testing.T) { tests := []struct { status PlayStatus isPlaying bool isPaused bool isStopped bool toString string }{ {PlayStatusPlaying, true, false, false, "Playing"}, {PlayStatusPaused, false, true, false, "Paused"}, {PlayStatusStopped, false, false, true, "Stopped"}, {PlayStatusBuffering, false, false, false, "Buffering"}, {PlayStatusInvalidPlay, false, false, false, "Invalid"}, {PlayStatus("UNKNOWN"), false, false, false, "Unknown"}, } for _, tt := range tests { t.Run(tt.toString, func(t *testing.T) { if tt.status.IsPlaying() != tt.isPlaying { t.Errorf("IsPlaying() = %v, want %v", tt.status.IsPlaying(), tt.isPlaying) } if tt.status.IsPaused() != tt.isPaused { t.Errorf("IsPaused() = %v, want %v", tt.status.IsPaused(), tt.isPaused) } if tt.status.IsStopped() != tt.isStopped { t.Errorf("IsStopped() = %v, want %v", tt.status.IsStopped(), tt.isStopped) } if tt.status.String() != tt.toString { t.Errorf("String() = %v, want %v", tt.status.String(), tt.toString) } }) } } func TestShuffleSetting_UnmarshalXML(t *testing.T) { tests := []struct { name string xmlInput string expected ShuffleSetting }{ { name: "shuffle on", xmlInput: `SHUFFLE_ON`, expected: ShuffleOn, }, { name: "shuffle off", xmlInput: `SHUFFLE_OFF`, expected: ShuffleOff, }, { name: "unknown state defaults to off", xmlInput: `UNKNOWN`, expected: ShuffleOff, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var setting ShuffleSetting err := xml.Unmarshal([]byte(tt.xmlInput), &setting) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } if setting != tt.expected { t.Errorf("Expected %s, got %s", tt.expected, setting) } }) } } func TestRepeatSetting_UnmarshalXML(t *testing.T) { tests := []struct { name string xmlInput string expected RepeatSetting }{ { name: "repeat off", xmlInput: `REPEAT_OFF`, expected: RepeatOff, }, { name: "repeat one", xmlInput: `REPEAT_ONE`, expected: RepeatOne, }, { name: "repeat all", xmlInput: `REPEAT_ALL`, expected: RepeatAll, }, { name: "unknown state defaults to off", xmlInput: `UNKNOWN`, expected: RepeatOff, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var setting RepeatSetting err := xml.Unmarshal([]byte(tt.xmlInput), &setting) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } if setting != tt.expected { t.Errorf("Expected %s, got %s", tt.expected, setting) } }) } } func TestNowPlaying_UnmarshalXML(t *testing.T) { xmlData := ` SYML https://i.scdn.co/image/ab67616d0000b273ca6f00df62ef197fdc8af79c In Between Breaths - Paris Unplugged SYML Paris Unplugged https://i.scdn.co/image/ab67616d0000b273ca6f00df62ef197fdc8af79c PLAY_STATE SHUFFLE_OFF REPEAT_OFF TRACK_ONDEMAND spotify:track:3LX0dk3YT8cUgp7XxUJgTB ` var nowPlaying NowPlaying err := xml.Unmarshal([]byte(xmlData), &nowPlaying) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } // Test basic fields if nowPlaying.DeviceID != "AABBCCDDEEFF" { t.Errorf("Expected DeviceID 'AABBCCDDEEFF', got '%s'", nowPlaying.DeviceID) } if nowPlaying.Source != "SPOTIFY" { t.Errorf("Expected Source 'SPOTIFY', got '%s'", nowPlaying.Source) } if nowPlaying.Track != "In Between Breaths - Paris Unplugged" { t.Errorf("Expected Track 'In Between Breaths - Paris Unplugged', got '%s'", nowPlaying.Track) } if nowPlaying.Artist != "SYML" { t.Errorf("Expected Artist 'SYML', got '%s'", nowPlaying.Artist) } if nowPlaying.Album != "Paris Unplugged" { t.Errorf("Expected Album 'Paris Unplugged', got '%s'", nowPlaying.Album) } if nowPlaying.SourceAccount != "user@example.com" { t.Errorf("Expected SourceAccount 'user@example.com', got '%s'", nowPlaying.SourceAccount) } if nowPlaying.PlayStatus != PlayStatusPlaying { t.Errorf("Expected PlayStatus Playing, got %v", nowPlaying.PlayStatus) } if nowPlaying.ShuffleSetting != ShuffleOff { t.Errorf("Expected ShuffleSetting Off, got %v", nowPlaying.ShuffleSetting) } if nowPlaying.RepeatSetting != RepeatOff { t.Errorf("Expected RepeatSetting Off, got %v", nowPlaying.RepeatSetting) } // Test ContentItem if nowPlaying.ContentItem == nil { t.Fatal("Expected ContentItem to be present") } if nowPlaying.ContentItem.Source != "SPOTIFY" { t.Errorf("Expected ContentItem.Source 'SPOTIFY', got '%s'", nowPlaying.ContentItem.Source) } if nowPlaying.ContentItem.ItemName != "SYML" { t.Errorf("Expected ContentItem.ItemName 'SYML', got '%s'", nowPlaying.ContentItem.ItemName) } // Test Art if nowPlaying.Art == nil { t.Fatal("Expected Art to be present") } if nowPlaying.Art.ArtImageStatus != "IMAGE_PRESENT" { t.Errorf("Expected Art.ArtImageStatus 'IMAGE_PRESENT', got '%s'", nowPlaying.Art.ArtImageStatus) } // Test Time if nowPlaying.Time == nil { t.Fatal("Expected Time to be present") } if nowPlaying.Time.Position != 36 { t.Errorf("Expected Time.Position 36, got %d", nowPlaying.Time.Position) } if nowPlaying.Time.Total != 210 { t.Errorf("Expected Time.Total 210, got %d", nowPlaying.Time.Total) } // Test TrackID if nowPlaying.TrackID != "spotify:track:3LX0dk3YT8cUgp7XxUJgTB" { t.Errorf("Expected TrackID 'spotify:track:3LX0dk3YT8cUgp7XxUJgTB', got '%s'", nowPlaying.TrackID) } // Test Capabilities if nowPlaying.SkipEnabled == nil { t.Error("Expected SkipEnabled to be present") } if nowPlaying.FavoriteEnabled == nil { t.Error("Expected FavoriteEnabled to be present") } if nowPlaying.SkipPreviousEnabled == nil { t.Error("Expected SkipPreviousEnabled to be present") } if nowPlaying.SeekSupported == nil { t.Error("Expected SeekSupported to be present") } else if !nowPlaying.SeekSupported.Value { t.Error("Expected SeekSupported.Value to be true") } } func TestNowPlaying_RadioStation(t *testing.T) { xmlData := ` Classic Rock 101.5 The Best Classic Rock Hits New York, NY https://cdn-radiotime-logos.tunein.com/s123456q.png PLAY_STATE ` var nowPlaying NowPlaying err := xml.Unmarshal([]byte(xmlData), &nowPlaying) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } if !nowPlaying.IsRadio() { t.Error("Expected IsRadio() to return true for TUNEIN source") } if nowPlaying.StationName != "Classic Rock 101.5" { t.Errorf("Expected StationName 'Classic Rock 101.5', got '%s'", nowPlaying.StationName) } if nowPlaying.Description != "The Best Classic Rock Hits" { t.Errorf("Expected Description 'The Best Classic Rock Hits', got '%s'", nowPlaying.Description) } if nowPlaying.StationLocation != "New York, NY" { t.Errorf("Expected StationLocation 'New York, NY', got '%s'", nowPlaying.StationLocation) } } func TestNowPlaying_EmptyState(t *testing.T) { xmlData := ` STOP_STATE ` var nowPlaying NowPlaying err := xml.Unmarshal([]byte(xmlData), &nowPlaying) if err != nil { t.Fatalf("Failed to unmarshal XML: %v", err) } if !nowPlaying.IsEmpty() { t.Error("Expected IsEmpty() to return true for empty state") } if nowPlaying.HasTrackInfo() { t.Error("Expected HasTrackInfo() to return false for empty state") } if nowPlaying.PlayStatus != PlayStatusStopped { t.Errorf("Expected PlayStatus Stopped, got %v", nowPlaying.PlayStatus) } } func TestNowPlaying_HelperMethods(t *testing.T) { // Test with track info nowPlaying := NowPlaying{ Track: "Test Track", Artist: "Test Artist", Album: "Test Album", Position: &Position{Position: 125}, } if nowPlaying.GetDisplayTitle() != "Test Track" { t.Errorf("Expected GetDisplayTitle() 'Test Track', got '%s'", nowPlaying.GetDisplayTitle()) } if nowPlaying.GetDisplayArtist() != "Test Artist" { t.Errorf("Expected GetDisplayArtist() 'Test Artist', got '%s'", nowPlaying.GetDisplayArtist()) } if !nowPlaying.HasTrackInfo() { t.Error("Expected HasTrackInfo() to return true") } if nowPlaying.IsEmpty() { t.Error("Expected IsEmpty() to return false") } // Test position formatting expectedPosition := "2:05" if nowPlaying.FormatPosition() != expectedPosition { t.Errorf("Expected FormatPosition() '%s', got '%s'", expectedPosition, nowPlaying.FormatPosition()) } // Test position duration expectedDuration := 125 * time.Second if nowPlaying.GetPositionDuration() != expectedDuration { t.Errorf("Expected GetPositionDuration() %v, got %v", expectedDuration, nowPlaying.GetPositionDuration()) } // Test with station name but no track radioNowPlaying := NowPlaying{ StationName: "Test Station", Source: "TUNEIN", } if radioNowPlaying.GetDisplayTitle() != "Test Station" { t.Errorf("Expected GetDisplayTitle() 'Test Station', got '%s'", radioNowPlaying.GetDisplayTitle()) } if !radioNowPlaying.IsRadio() { t.Error("Expected IsRadio() to return true for TUNEIN source") } // Test with ContentItem fallback contentNowPlaying := NowPlaying{ ContentItem: &ContentItem{ItemName: "Content Item Name"}, } if contentNowPlaying.GetDisplayTitle() != "Content Item Name" { t.Errorf("Expected GetDisplayTitle() 'Content Item Name', got '%s'", contentNowPlaying.GetDisplayTitle()) } // Test artwork URL artNowPlaying := NowPlaying{ Art: &Art{URL: "https://example.com/art.jpg"}, } if artNowPlaying.GetArtworkURL() != "https://example.com/art.jpg" { t.Errorf("Expected GetArtworkURL() 'https://example.com/art.jpg', got '%s'", artNowPlaying.GetArtworkURL()) } // Test ContentItem artwork fallback contentArtNowPlaying := NowPlaying{ ContentItem: &ContentItem{ContainerArt: "https://example.com/container.jpg"}, } if contentArtNowPlaying.GetArtworkURL() != "https://example.com/container.jpg" { t.Errorf("Expected GetArtworkURL() 'https://example.com/container.jpg', got '%s'", contentArtNowPlaying.GetArtworkURL()) } } func TestNowPlaying_EdgeCases(t *testing.T) { // Test with nil time and position nowPlaying := NowPlaying{} if nowPlaying.FormatPosition() != "" { t.Errorf("Expected FormatPosition() to return empty string for nil time/position, got '%s'", nowPlaying.FormatPosition()) } if nowPlaying.GetPositionDuration() != 0 { t.Errorf("Expected GetPositionDuration() to return 0 for nil time/position, got %v", nowPlaying.GetPositionDuration()) } if nowPlaying.GetTotalDuration() != 0 { t.Errorf("Expected GetTotalDuration() to return 0 for nil time, got %v", nowPlaying.GetTotalDuration()) } // Test fallback to "Unknown" title emptyNowPlaying := NowPlaying{} if emptyNowPlaying.GetDisplayTitle() != "Unknown" { t.Errorf("Expected GetDisplayTitle() 'Unknown' for empty NowPlaying, got '%s'", emptyNowPlaying.GetDisplayTitle()) } // Test description fallback for artist descNowPlaying := NowPlaying{ Description: "Test Description", } if descNowPlaying.GetDisplayArtist() != "Test Description" { t.Errorf("Expected GetDisplayArtist() 'Test Description', got '%s'", descNowPlaying.GetDisplayArtist()) } } func TestNowPlaying_NewFields(t *testing.T) { // Test Time field nowPlaying := NowPlaying{ Time: &Time{ Total: 180, Position: 65, }, } if !nowPlaying.HasTimeInfo() { t.Error("Expected HasTimeInfo() to return true for Time field") } expectedDuration := "1:05 / 3:00" if nowPlaying.FormatDuration() != expectedDuration { t.Errorf("Expected FormatDuration() '%s', got '%s'", expectedDuration, nowPlaying.FormatDuration()) } // Test capabilities capableNowPlaying := NowPlaying{ SkipEnabled: &SkipEnabled{}, FavoriteEnabled: &FavoriteEnabled{}, SkipPreviousEnabled: &SkipPreviousEnabled{}, SeekSupported: &SeekSupported{Value: true}, } if !capableNowPlaying.CanSkip() { t.Error("Expected CanSkip() to return true") } if !capableNowPlaying.CanFavorite() { t.Error("Expected CanFavorite() to return true") } if !capableNowPlaying.CanSkipPrevious() { t.Error("Expected CanSkipPrevious() to return true") } if !capableNowPlaying.IsSeekSupported() { t.Error("Expected IsSeekSupported() to return true") } // Test seek not supported noSeekNowPlaying := NowPlaying{ SeekSupported: &SeekSupported{Value: false}, } if noSeekNowPlaying.IsSeekSupported() { t.Error("Expected IsSeekSupported() to return false") } }