Files
Mrixs 6312c24b0f Fix Track model to match Python reference
- R128: changed from JSONR128 float64 to R128 struct with i, tp, important_secs fields
  (Python R128 is an object, not a number)
- StorageID -> StorageDir with json tag storage_dir (Python API uses storage_dir)
- Lyrics: replaced TrackLyricsInfo with Lyrics (from supplement/lyrics.py) and
  TrackLyricsInfo (from track/lyrics_info.py with has_available_sync_lyrics,
  has_available_text_lyrics fields)
- Track.Lyrics field now maps to lyrics_info JSON key with TrackLyricsInfo type
2026-10-18 12:34:58 +03:00

515 lines
14 KiB
Go

package client
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
)
func TestClient_GetTracks(t *testing.T) {
tests := []struct {
name string
trackIDs []string
mockResponse []byte
mockError error
expectError bool
expectedTracks int
checkTrack func(*testing.T, *models.Track)
}{
{
name: "single track",
trackIDs: []string{"12345"},
mockResponse: []byte(`{
"result": [
{
"id": "12345",
"title": "Test Track",
"duration_ms": 180000,
"storage_dir": "67890",
"cover_uri": "https://example.com/cover.jpg",
"genre": "rock",
"version": "album",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": -12.5,
"track_sharing_flag": "allowed",
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
"lyrics_info": {"has_available_sync_lyrics": true, "has_available_text_lyrics": true}
}
]
}`),
expectedTracks: 1,
checkTrack: func(t *testing.T, tr *models.Track) {
if tr.ID != "12345" {
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
}
if tr.Title != "Test Track" {
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
}
if tr.DurationMs != 180000 {
t.Errorf("Expected DurationMs 180000, got %d", tr.DurationMs)
}
if len(tr.Artists) != 1 {
t.Errorf("Expected 1 artist, got %d", len(tr.Artists))
}
if tr.Major == nil || tr.Major.Name != "Major Label" {
t.Errorf("Expected Major 'Major Label'")
}
if tr.LyricsInfo == nil || tr.LyricsInfo.HasAvailableSyncLyrics != true {
t.Errorf("Expected LyricsInfo.HasAvailableSyncLyrics true")
}
},
},
{
name: "multiple tracks",
trackIDs: []string{"12345", "67890"},
mockResponse: []byte(`{
"result": [
{"id": "12345", "title": "Track 1", "duration_ms": 100000, "storage_dir": "1", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []},
{"id": "67890", "title": "Track 2", "duration_ms": 200000, "storage_dir": "2", "cover_uri": "", "genre": "", "version": "", "explicit": false, "available": true, "available_for_premium_users": true, "r128": 0, "track_sharing_flag": "", "artists": [], "albums": []}
]
}`),
expectedTracks: 2,
checkTrack: func(t *testing.T, tr *models.Track) {
if tr.ID != "12345" && tr.ID != "67890" {
t.Errorf("Unexpected track ID: %s", tr.ID)
}
},
},
{
name: "empty track IDs",
trackIDs: []string{},
mockResponse: nil,
expectedTracks: 0,
},
{
name: "nil track IDs",
trackIDs: nil,
mockResponse: nil,
expectedTracks: 0,
},
{
name: "API error",
trackIDs: []string{"12345"},
mockError: utils.NewYandexMusicError("not found", utils.ErrNotFound),
expectError: true,
},
{
name: "empty result",
trackIDs: []string{"nonexistent"},
mockResponse: []byte(`{"result": []}`),
expectedTracks: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var server *httptest.Server
if tt.mockError != nil || tt.mockResponse != nil {
server = testServer(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tracks" {
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
}
if tt.mockError != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"error": "not found"}`))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(tt.mockResponse)
})
defer server.Close()
}
var client *Client
if server != nil {
client = newTestClient(server.URL)
} else {
client = newTestClient("http://localhost:9999")
}
tracks, err := client.GetTracks(context.Background(), tt.trackIDs)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(tracks) != tt.expectedTracks {
t.Errorf("Expected %d tracks, got %d", tt.expectedTracks, len(tracks))
return
}
if tt.checkTrack != nil {
for _, track := range tracks {
tt.checkTrack(t, track)
}
}
// Verify client is set on tracks
for _, track := range tracks {
if track.GetClient() == nil {
t.Error("Client not set on track")
}
}
})
}
}
func TestClient_GetTrack(t *testing.T) {
tests := []struct {
name string
trackID string
mockResponse []byte
mockError error
expectError bool
checkTrack func(*testing.T, *models.Track)
}{
{
name: "success",
trackID: "12345",
mockResponse: []byte(`{
"result": [
{
"id": "12345",
"title": "Test Track",
"duration_ms": 180000,
"storage_dir": "67890",
"cover_uri": "https://example.com/cover.jpg",
"genre": "rock",
"version": "album",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": -12.5,
"track_sharing_flag": "allowed",
"artists": [{"id": "1", "name": "Artist 1", "cover_uri": "https://example.com/artist.jpg"}],
"albums": [{"id": "789", "title": "Test Album", "cover_uri": "https://example.com/album.jpg", "year": 2023}],
"major": {"id": "major1", "name": "Major Label", "description": "Big label"},
"lyrics_info": {"has_available_sync_lyrics": true, "has_available_text_lyrics": true}
}
]
}`),
checkTrack: func(t *testing.T, tr *models.Track) {
if tr.ID != "12345" {
t.Errorf("Expected ID '12345', got '%s'", tr.ID)
}
if tr.Title != "Test Track" {
t.Errorf("Expected Title 'Test Track', got '%s'", tr.Title)
}
},
},
{
name: "not found",
trackID: "nonexistent",
mockResponse: []byte(`{"result": []}`),
expectError: true,
},
{
name: "API error",
trackID: "12345",
mockError: utils.NewYandexMusicError("unauthorized", utils.ErrUnauthorized),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var server *httptest.Server
if tt.mockError != nil || tt.mockResponse != nil {
server = testServer(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tracks" {
t.Errorf("Expected path '/tracks', got '%s'", r.URL.Path)
}
if r.URL.Query().Get("track-ids") != tt.trackID {
t.Errorf("Expected track-ids '%s', got '%s'", tt.trackID, r.URL.Query().Get("track-ids"))
}
if tt.mockError != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"error": "unauthorized"}`))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(tt.mockResponse)
})
defer server.Close()
}
var client *Client
if server != nil {
client = newTestClient(server.URL)
} else {
client = newTestClient("http://localhost:9999")
}
track, err := client.GetTrack(context.Background(), tt.trackID)
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if track == nil {
t.Fatal("Expected track, got nil")
}
if tt.checkTrack != nil {
tt.checkTrack(t, track)
}
if track.GetClient() == nil {
t.Error("Client not set on track")
}
})
}
}
func TestClient_SearchTracks(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/search" {
http.NotFound(w, r)
return
}
query := r.URL.Query()
if query.Get("type") != "track" {
t.Errorf("Expected type=track, got %s", query.Get("type"))
}
page := query.Get("page")
pageSize := query.Get("page_size")
response := map[string]interface{}{
"result": map[string]interface{}{
"tracks": map[string]interface{}{
"total": 2,
"results": []map[string]interface{}{
{
"id": "1",
"title": "Track 1",
"duration_ms": 100000,
"storage_dir": "1",
"cover_uri": "",
"genre": "",
"version": "",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": 0,
"track_sharing_flag": "",
"artists": []interface{}{},
"albums": []interface{}{},
},
{
"id": "2",
"title": "Track 2",
"duration_ms": 200000,
"storage_dir": "2",
"cover_uri": "",
"genre": "",
"version": "",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": 0,
"track_sharing_flag": "",
"artists": []interface{}{},
"albums": []interface{}{},
},
},
},
},
}
// Verify pagination
if page != "1" || pageSize != "20" {
t.Errorf("Unexpected pagination: page=%s, page_size=%s", page, pageSize)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Create client with test server URL
config := ClientConfig{
BaseURL: server.URL,
Token: "test-token",
}
client := NewClient(config)
ctx := context.Background()
result, err := client.SearchTracks(ctx, "test query", 1, 20)
if err != nil {
t.Fatalf("SearchTracks failed: %v", err)
}
if result == nil {
t.Fatal("Expected search result, got nil")
}
if result.Total != 2 {
t.Errorf("Expected total 2, got %d", result.Total)
}
if len(result.Results) != 2 {
t.Errorf("Expected 2 results, got %d", len(result.Results))
}
for i, track := range result.Results {
if track.GetClient() == nil {
t.Errorf("Track %d: Client not set", i)
}
if track.ID != "1" && track.ID != "2" {
t.Errorf("Track %d: Unexpected ID %s", i, track.ID)
}
}
}
func TestClient_SearchTracks_Mocked(t *testing.T) {
server := testServer(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/search" {
t.Errorf("Unexpected request: %s %s", r.Method, r.URL.Path)
return
}
if r.URL.Query().Get("type") != "track" {
t.Errorf("Expected type=track, got %s", r.URL.Query().Get("type"))
}
mockSearchResult := map[string]interface{}{
"result": map[string]interface{}{
"tracks": map[string]interface{}{
"total": 2,
"results": []map[string]interface{}{
{
"id": "1",
"title": "Track 1",
"duration_ms": 100000,
"storage_dir": "1",
"cover_uri": "",
"genre": "",
"version": "",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": 0,
"track_sharing_flag": "",
"artists": []interface{}{},
"albums": []interface{}{},
},
{
"id": "2",
"title": "Track 2",
"duration_ms": 200000,
"storage_dir": "2",
"cover_uri": "",
"genre": "",
"version": "",
"explicit": false,
"available": true,
"available_for_premium_users": true,
"r128": 0,
"track_sharing_flag": "",
"artists": []interface{}{},
"albums": []interface{}{},
},
},
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(mockSearchResult)
})
defer server.Close()
client := newTestClient(server.URL)
ctx := context.Background()
result, err := client.SearchTracks(ctx, "test query", 1, 20)
if err != nil {
t.Fatalf("SearchTracks failed: %v", err)
}
if result == nil {
t.Fatal("Expected search result, got nil")
}
if result.Total != 2 {
t.Errorf("Expected total 2, got %d", result.Total)
}
if len(result.Results) != 2 {
t.Errorf("Expected 2 results, got %d", len(result.Results))
}
for i, track := range result.Results {
if track.GetClient() == nil {
t.Errorf("Track %d: Client not set", i)
}
}
// Test error case
errorServer := testServer(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"error": "network error"}`))
})
defer errorServer.Close()
errorClient := newTestClient(errorServer.URL)
_, err = errorClient.SearchTracks(ctx, "test", 1, 20)
if err == nil {
t.Error("Expected error for network failure")
}
if !utils.IsNetworkError(err) {
t.Errorf("Expected network error, got %v", err)
}
}
func TestJoinIDs(t *testing.T) {
tests := []struct {
name string
input []string
expected string
}{
{"empty", []string{}, ""},
{"single", []string{"123"}, "123"},
{"multiple", []string{"123", "456", "789"}, "123,456,789"},
{"with empty", []string{"123", "", "456"}, "123,,456"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := joinIDs(tt.input)
if result != tt.expected {
t.Errorf("joinIDs(%v) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}