mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
696 lines
23 KiB
Go
696 lines
23 KiB
Go
package deezer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"streamrip-go/internal/jsonutil"
|
|
|
|
"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 TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
|
callCount := 0
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/artist/9":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Lost Frequencies"})
|
|
case "/artist/9/albums":
|
|
callCount++
|
|
index := r.URL.Query().Get("index")
|
|
limit := r.URL.Query().Get("limit")
|
|
if limit != "100" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad limit"}})
|
|
return
|
|
}
|
|
switch index {
|
|
case "0":
|
|
items := make([]any, 0, 100)
|
|
for i := 0; i < 100; i++ {
|
|
items = append(items, map[string]any{"id": i + 1, "title": "Album"})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": items, "total": 101})
|
|
case "100":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 101, "title": "Album 101"}}, "total": 101})
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
default:
|
|
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 }()
|
|
|
|
meta, err := c.GetMetadata(context.Background(), "9", "artist")
|
|
if err != nil {
|
|
t.Fatalf("GetMetadata() error = %v", err)
|
|
}
|
|
albumsObj, _ := meta["albums"].(map[string]any)
|
|
items, _ := albumsObj["items"].([]any)
|
|
if len(items) != 101 {
|
|
t.Fatalf("albums len = %d, want 101", len(items))
|
|
}
|
|
if got := strings.TrimSpace(jsonutil.StringFromAny(meta["name"])); got != "Lost Frequencies" {
|
|
t.Fatalf("artist name = %q, want Lost Frequencies", got)
|
|
}
|
|
if callCount != 2 {
|
|
t.Fatalf("call count = %d, want 2", callCount)
|
|
}
|
|
}
|
|
|
|
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":
|
|
if got := strings.TrimSpace(r.Header.Get("Accept-Charset")); got != "UTF-8" {
|
|
t.Fatalf("accept-charset = %q, want UTF-8", got)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
media, _ := payload["media"].([]any)
|
|
if len(media) != 1 {
|
|
t.Fatalf("media length = %d, want 1", len(media))
|
|
}
|
|
entry, _ := media[0].(map[string]any)
|
|
formats, _ := entry["formats"].([]any)
|
|
if len(formats) != 6 {
|
|
t.Fatalf("formats length = %d, want 6", len(formats))
|
|
}
|
|
_ = 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 TestLoginPrefersARLFlowOverRefreshShortcut(t *testing.T) {
|
|
mobileToken := testMobileToken(t)
|
|
refreshCalled := false
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/web":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"USER_ID": "42", "JWT": "jwt-from-web", "refresh_token": "refresh-from-web", "license_token": "license-from-web"}})
|
|
case "/gateway":
|
|
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_userAutolog":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"JWT": "jwt-from-autolog", "license_token": "license-from-autolog", "refresh_token": "refresh-from-autolog"}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
case "/pipe":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-from-pipe"}}}})
|
|
case "/renew":
|
|
refreshCalled = true
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.ARL = "arl"
|
|
cfgData.Deezer.RefreshToken = "refresh-token"
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
|
|
origGateway := gatewayURL
|
|
origWeb := webGWLight
|
|
origPipe := pipeURL
|
|
origAuth := authURL
|
|
gatewayURL = ts.URL + "/gateway"
|
|
webGWLight = ts.URL + "/web"
|
|
pipeURL = ts.URL + "/pipe"
|
|
authURL = ts.URL + "/renew"
|
|
defer func() {
|
|
gatewayURL = origGateway
|
|
webGWLight = origWeb
|
|
pipeURL = origPipe
|
|
authURL = origAuth
|
|
}()
|
|
|
|
if err := c.Login(context.Background()); err != nil {
|
|
t.Fatalf("Login() error = %v", err)
|
|
}
|
|
if refreshCalled {
|
|
t.Fatalf("expected ARL launch flow without refresh shortcut")
|
|
}
|
|
if c.license != "license-from-pipe" {
|
|
t.Fatalf("license = %q, want license-from-pipe", c.license)
|
|
}
|
|
}
|
|
|
|
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 TestGetTrackTokenPrefersPipeToken(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/pipe":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"media": map[string]any{"token": map[string]any{"payload": "pipe-track-token"}}}}})
|
|
case "/track/42":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "track_token": "api-track-token"})
|
|
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-token"
|
|
|
|
origBase := baseURL
|
|
origPipe := pipeURL
|
|
baseURL = ts.URL
|
|
pipeURL = ts.URL + "/pipe"
|
|
defer func() {
|
|
baseURL = origBase
|
|
pipeURL = origPipe
|
|
}()
|
|
|
|
token, err := c.getTrackToken(context.Background(), "42")
|
|
if err != nil {
|
|
t.Fatalf("getTrackToken() error = %v", err)
|
|
}
|
|
if token != "pipe-track-token" {
|
|
t.Fatalf("token = %q, want pipe-track-token", token)
|
|
}
|
|
}
|
|
|
|
func TestGetDownloadableUsesPipeTrackToken(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": "api-track-token"})
|
|
case "/pipe":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"media": map[string]any{"token": map[string]any{"payload": "pipe-track-token"}}}}})
|
|
case "/media":
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
tokens, _ := payload["track_tokens"].([]any)
|
|
if len(tokens) == 0 || strings.TrimSpace(jsonutil.StringFromAny(tokens[0])) != "pipe-track-token" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{map[string]any{"code": 2004, "message": "The track country differs from the license."}}, "media": []any{}}}})
|
|
return
|
|
}
|
|
_ = 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.URL != "https://cdn.example/file" || d.Extension != "flac" {
|
|
t.Fatalf("unexpected downloadable: %+v", d)
|
|
}
|
|
}
|
|
|
|
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(jsonutil.StringFromAny(meta["lyrics"]), "Go shawty") {
|
|
t.Fatalf("expected lyrics text, got %q", jsonutil.StringFromAny(meta["lyrics"]))
|
|
}
|
|
if !strings.Contains(jsonutil.StringFromAny(meta["lyrics_synced"]), "[00:00.00]Go, go, go") {
|
|
t.Fatalf("expected synced lyrics, got %q", jsonutil.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(jsonutil.StringFromAny(payload["mail"])) == "" || strings.TrimSpace(jsonutil.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 TestMobileAuthIncludesAppContextParams(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
|
|
}
|
|
if r.URL.Query().Get("method") != "mobile_auth" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("version")); got != deezerAppVersion {
|
|
t.Fatalf("version = %q, want %q", got, deezerAppVersion)
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("lang")); got != deezerAppLang {
|
|
t.Fatalf("lang = %q, want %q", got, deezerAppLang)
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("buildId")); got != deezerBuildID {
|
|
t.Fatalf("buildId = %q, want %q", got, deezerBuildID)
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("screenWidth")); got != deezerScreenW {
|
|
t.Fatalf("screenWidth = %q, want %q", got, deezerScreenW)
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("screenHeight")); got != deezerScreenH {
|
|
t.Fatalf("screenHeight = %q, want %q", got, deezerScreenH)
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("uniq_id")); got == "" {
|
|
t.Fatalf("uniq_id is empty")
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"TOKEN": mobileToken}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
|
|
origGateway := gatewayURL
|
|
gatewayURL = ts.URL + "/gateway"
|
|
defer func() { gatewayURL = origGateway }()
|
|
|
|
token, err := c.mobileAuth(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("mobileAuth() error = %v", err)
|
|
}
|
|
if token != mobileToken {
|
|
t.Fatalf("token = %q, want %q", token, mobileToken)
|
|
}
|
|
}
|
|
|
|
func TestMobileUserAutologIncludesRefreshToken(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/gateway" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
if method := r.URL.Query().Get("method"); method != "mobile_userAutolog" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if got := strings.TrimSpace(r.URL.Query().Get("arl")); got != "" {
|
|
t.Fatalf("unexpected arl query parameter: %q", got)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if got := strings.TrimSpace(jsonutil.StringFromAny(payload["refresh_token"])); got != "refresh-token" {
|
|
t.Fatalf("refresh_token payload = %q, want refresh-token", got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"JWT": "jwt-token", "license_token": "license-token"}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.sid = "sid123"
|
|
c.userID = "42"
|
|
c.arl = "arl-token"
|
|
c.refresh = "refresh-token"
|
|
|
|
origGateway := gatewayURL
|
|
gatewayURL = ts.URL + "/gateway"
|
|
defer func() { gatewayURL = origGateway }()
|
|
|
|
if err := c.mobileUserAutolog(context.Background()); err != nil {
|
|
t.Fatalf("mobileUserAutolog() error = %v", err)
|
|
}
|
|
if c.jwt != "jwt-token" {
|
|
t.Fatalf("jwt = %q, want jwt-token", c.jwt)
|
|
}
|
|
if c.license != "license-token" {
|
|
t.Fatalf("license = %q, want license-token", c.license)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestRefreshJWTHTTPError(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad refresh"}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.refresh = "refresh-token"
|
|
|
|
origAuth := authURL
|
|
authURL = ts.URL
|
|
defer func() { authURL = origAuth }()
|
|
|
|
err := c.refreshJWT(context.Background())
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "status=401") {
|
|
t.Fatalf("expected http status error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRefreshLicenseFromPipeGraphQLError(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"errors": []any{map[string]any{"message": "token expired"}}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.jwt = "jwt-token"
|
|
|
|
origPipe := pipeURL
|
|
pipeURL = ts.URL
|
|
defer func() { pipeURL = origPipe }()
|
|
|
|
err := c.refreshLicenseFromPipe(context.Background())
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "token expired") {
|
|
t.Fatalf("expected graphql error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRefreshLicenseFromPipeUsesMobileGraphQLShape(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := strings.TrimSpace(r.Header.Get("Authorization")); got != "Bearer jwt-token" {
|
|
t.Fatalf("authorization = %q, want Bearer jwt-token", got)
|
|
}
|
|
if got := r.Header.Get("Accept"); !strings.Contains(got, "application/graphql-response+json") {
|
|
t.Fatalf("accept header = %q", got)
|
|
}
|
|
if got := strings.TrimSpace(r.Header.Get("Accept-Language")); got != "en-US" {
|
|
t.Fatalf("accept-language = %q, want en-US", got)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
op := strings.TrimSpace(jsonutil.StringFromAny(payload["operationName"]))
|
|
if op != "KmpMpMediaServiceLicenseToken" {
|
|
t.Fatalf("operationName = %q, want KmpMpMediaServiceLicenseToken", op)
|
|
}
|
|
ext, _ := payload["extensions"].(map[string]any)
|
|
clientLib, _ := ext["clientLibrary"].(map[string]any)
|
|
if got := strings.TrimSpace(jsonutil.StringFromAny(clientLib["name"])); got != "apollo-kotlin" {
|
|
t.Fatalf("clientLibrary.name = %q, want apollo-kotlin", got)
|
|
}
|
|
if got := strings.TrimSpace(jsonutil.StringFromAny(clientLib["version"])); got != "4.4.2" {
|
|
t.Fatalf("clientLibrary.version = %q, want 4.4.2", got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-token"}}}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.jwt = "jwt-token"
|
|
|
|
origPipe := pipeURL
|
|
pipeURL = ts.URL
|
|
defer func() { pipeURL = origPipe }()
|
|
|
|
if err := c.refreshLicenseFromPipe(context.Background()); err != nil {
|
|
t.Fatalf("refreshLicenseFromPipe() error = %v", err)
|
|
}
|
|
if c.license != "license-token" {
|
|
t.Fatalf("license = %q, want license-token", c.license)
|
|
}
|
|
}
|