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) } if d.Audio.Container != "FLAC" || d.Audio.Quality != "LOSSLESS" { t.Fatalf("unexpected audio profile: %+v", d.Audio) } } 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) } }