package deezer import ( "context" "encoding/hex" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "streamrip-go/internal/config" ) func TestSearchTrack(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/search/track" { _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 1, "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}}}}) return } w.WriteHeader(http.StatusNotFound) })) defer ts.Close() cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true origBase := baseURL baseURL = ts.URL defer func() { baseURL = origBase }() pages, err := c.Search(context.Background(), "track", "dreams", 5) if err != nil { t.Fatalf("Search() error = %v", err) } if len(pages) != 1 { t.Fatalf("pages len = %d, want 1", len(pages)) } } func TestGetDownloadableNativeCipher(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/track/42": _ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"}) case "/media": _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}}) default: w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() cfgData := config.DefaultConfigData() cfgData.Deezer.ARL = "arl" c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.arl = "arl" c.license = "license" c.jwt = "jwt" origBase := baseURL origMedia := mediaURL origPipe := pipeURL baseURL = ts.URL mediaURL = ts.URL + "/media" pipeURL = ts.URL + "/pipe" defer func() { baseURL = origBase mediaURL = origMedia pipeURL = origPipe }() d, err := c.GetDownloadable(context.Background(), "42", 2) if err != nil { t.Fatalf("GetDownloadable() error = %v", err) } if d.Cipher != "BF_CBC_STRIPE" || d.Extension != "flac" || d.TrackID != "42" { t.Fatalf("unexpected downloadable: %+v", d) } } func TestGetDownloadableRequiresARL(t *testing.T) { cfgData := config.DefaultConfigData() cfgData.Deezer.ARL = "" c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true _, err := c.GetDownloadable(context.Background(), "42", 2) if err == nil || !strings.Contains(strings.ToLower(err.Error()), "arl") { t.Fatalf("expected arl requirement error, got %v", err) } } func TestGetDownloadableDRMError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/track/42": _ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"}) case "/media": _ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{map[string]any{"code": 403, "message": "DRM required"}}, "media": []any{}}}}) default: w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() cfgData := config.DefaultConfigData() cfgData.Deezer.ARL = "arl" c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.arl = "arl" c.license = "license" c.jwt = "jwt" origBase := baseURL origMedia := mediaURL origPipe := pipeURL baseURL = ts.URL mediaURL = ts.URL + "/media" pipeURL = ts.URL + "/pipe" defer func() { baseURL = origBase mediaURL = origMedia pipeURL = origPipe }() _, err := c.GetDownloadable(context.Background(), "42", 2) if err == nil || !strings.Contains(strings.ToLower(err.Error()), "drm") { t.Fatalf("expected drm error, got %v", err) } } func TestGetMetadataAddsLyricsFromPipe(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/track/1141668": _ = json.NewEncoder(w).Encode(map[string]any{"id": 1141668, "title": "In Da Club", "artist": map[string]any{"name": "50 Cent"}}) case "/pipe": _ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"lyrics": map[string]any{"text": "Go, go, go\nGo shawty", "synchronizedLines": []any{map[string]any{"line": "Go, go, go", "milliseconds": 0}, map[string]any{"line": "Go shawty", "milliseconds": 4280}}}}}}) default: w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.jwt = "jwt" origBase := baseURL origPipe := pipeURL baseURL = ts.URL pipeURL = ts.URL + "/pipe" defer func() { baseURL = origBase pipeURL = origPipe }() meta, err := c.GetMetadata(context.Background(), "1141668", "track") if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if !strings.Contains(stringFromAny(meta["lyrics"]), "Go shawty") { t.Fatalf("expected lyrics text, got %q", stringFromAny(meta["lyrics"])) } if !strings.Contains(stringFromAny(meta["lyrics_synced"]), "[00:00.00]Go, go, go") { t.Fatalf("expected synced lyrics, got %q", stringFromAny(meta["lyrics_synced"])) } } func TestLoginWithCredentials(t *testing.T) { mobileToken := testMobileToken(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/gateway" { w.WriteHeader(http.StatusNotFound) return } switch r.URL.Query().Get("method") { case "mobile_auth": _ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"TOKEN": mobileToken}}) case "api_checkToken": _ = json.NewEncoder(w).Encode(map[string]any{"results": "sid123"}) case "mobile_userAuth": var payload map[string]any _ = json.NewDecoder(r.Body).Decode(&payload) if strings.TrimSpace(stringFromAny(payload["mail"])) == "" || strings.TrimSpace(stringFromAny(payload["password"])) == "" { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "missing creds"}}) return } _ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"ARL": "arl-token", "JWT": "jwt-token", "refresh_token": "refresh-token", "license_token": "license-token", "USER_ID": "42"}}) default: w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() cfgData := config.DefaultConfigData() cfgData.Deezer.Email = "tidal1@alpin.sbs" cfgData.Deezer.Password = "tidal1@alpin.sbs" c := New(&config.Config{File: cfgData, Session: cfgData}) origGateway := gatewayURL gatewayURL = ts.URL + "/gateway" defer func() { gatewayURL = origGateway }() if err := c.Login(context.Background()); err != nil { t.Fatalf("Login() error = %v", err) } if !c.loggedIn { t.Fatalf("expected logged in client") } if c.arl != "arl-token" { t.Fatalf("arl = %q, want arl-token", c.arl) } if c.jwt != "jwt-token" { t.Fatalf("jwt = %q, want jwt-token", c.jwt) } if c.refresh != "refresh-token" { t.Fatalf("refresh = %q, want refresh-token", c.refresh) } if c.license != "license-token" { t.Fatalf("license = %q, want license-token", c.license) } if c.cfg.Session.Deezer.RefreshToken != "refresh-token" { t.Fatalf("session refresh token = %q", c.cfg.Session.Deezer.RefreshToken) } if c.cfg.File.Deezer.RefreshToken != "refresh-token" { t.Fatalf("file refresh token = %q", c.cfg.File.Deezer.RefreshToken) } } func testMobileToken(t *testing.T) string { t.Helper() plain := []byte(strings.Repeat("A", 64) + strings.Repeat("B", 16) + strings.Repeat("C", 16)) enc, err := aesECBEncrypt([]byte(gatewayDec), plain) if err != nil { t.Fatalf("aesECBEncrypt() error = %v", err) } return hex.EncodeToString(enc) } func TestLoginWithRefreshToken(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/renew": _ = json.NewEncoder(w).Encode(map[string]any{"jwt": "jwt-token", "refresh_token": "refresh-token-2"}) case "/pipe": _ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-token"}}}}) default: w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() cfgData := config.DefaultConfigData() cfgData.Deezer.RefreshToken = "refresh-token" c := New(&config.Config{File: cfgData, Session: cfgData}) origAuth := authURL origPipe := pipeURL authURL = ts.URL + "/renew" pipeURL = ts.URL + "/pipe" defer func() { authURL = origAuth pipeURL = origPipe }() if err := c.Login(context.Background()); err != nil { t.Fatalf("Login() error = %v", err) } if !c.loggedIn { t.Fatalf("expected logged in client") } if c.jwt != "jwt-token" || c.license != "license-token" { t.Fatalf("unexpected jwt/license: jwt=%q license=%q", c.jwt, c.license) } if c.cfg.Session.Deezer.RefreshToken != "refresh-token-2" { t.Fatalf("session refresh token = %q", c.cfg.Session.Deezer.RefreshToken) } }