mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-16 06:45:26 +02:00
141 lines
6.2 KiB
Go
141 lines
6.2 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
func TestFallbackKeepsPendingVerificationOwnership(t *testing.T) {
|
|
for _, mode := range []string{"fresh-response", "saved-challenge", "cached-unavailable", "cold-session", "network-error", "download-error", "empty-download-error"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
primary := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
|
primary.ID, primary.Manifest.Name = "primary-provider", "primary-provider"
|
|
secondary := newTestLoadedExtension(t, ExtensionTypeMetadataProvider, ExtensionTypeDownloadProvider)
|
|
secondary.ID, secondary.Manifest.Name = "secondary-provider", "secondary-provider"
|
|
secondary.Manifest.Permissions.Storage = true
|
|
secondary.Manifest.SignedSession = &SignedSessionConfig{
|
|
Namespace: "fixture-session", BaseURL: "https://auth.example.test",
|
|
}
|
|
last := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
|
last.ID, last.Manifest.Name = "last-provider", "last-provider"
|
|
for ext, script := range map[*loadedExtension]string{
|
|
primary: `registerExtension({
|
|
checkAvailability: function() { return {available: true, track_id: "source-track"}; },
|
|
download: function() { return {success: false, error_type: "api_error", error_message: "primary request failed", retry_after_seconds: 37}; }
|
|
});`,
|
|
secondary: `var savedChallenge;
|
|
function queryCatalog() {
|
|
var response = session.signedFetch("GET", "/catalog");
|
|
if (response.needsVerification) {
|
|
savedChallenge = new Error("VERIFY_REQUIRED");
|
|
throw savedChallenge;
|
|
}
|
|
throw new Error("fixture expected a challenge");
|
|
}
|
|
registerExtension({
|
|
searchTracks: queryCatalog,
|
|
checkAvailability: function() {
|
|
if (fixtureMode === "cached-unavailable" || fixtureMode === "cold-session") return {available: false, reason: "No verified track match found"};
|
|
if (fixtureMode === "network-error") throw new Error("lookup network timeout");
|
|
if (fixtureMode === "download-error" || fixtureMode === "empty-download-error") return {available: true, track_id: "matched-track"};
|
|
if (fixtureMode === "saved-challenge") throw savedChallenge;
|
|
return queryCatalog();
|
|
},
|
|
download: function() {
|
|
if (fixtureMode === "download-error") throw new Error("download network timeout");
|
|
if (fixtureMode === "empty-download-error") return {success: false};
|
|
throw new Error("verification must finish before download");
|
|
}
|
|
});`,
|
|
last: `registerExtension({checkAvailability: function() { recordLastProvider(); return {available: false}; }});`,
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(ext.SourceDir, "index.js"), fmt.Appendf(nil, "var fixtureMode = %q;\n%s", mode, script), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
manager := getExtensionManager()
|
|
manager.mu.Lock()
|
|
previousExtensions := manager.extensions
|
|
manager.extensions = map[string]*loadedExtension{primary.ID: primary, secondary.ID: secondary, last.ID: last}
|
|
manager.mu.Unlock()
|
|
previousPriority, previousFallback := GetProviderPriority(), GetExtensionFallbackProviderIDs()
|
|
SetProviderPriority([]string{secondary.ID, last.ID, primary.ID})
|
|
SetExtensionFallbackProviderIDs(nil)
|
|
t.Cleanup(func() {
|
|
for _, ext := range []*loadedExtension{primary, secondary, last} {
|
|
ClearPendingAuthRequest(ext.ID)
|
|
teardownExtension(ext)
|
|
}
|
|
manager.mu.Lock()
|
|
manager.extensions = previousExtensions
|
|
manager.mu.Unlock()
|
|
SetProviderPriority(previousPriority)
|
|
SetExtensionFallbackProviderIDs(previousFallback)
|
|
resetPreparedDownloadRequestCacheForTest()
|
|
})
|
|
if err := secondary.ensureRuntimeReady(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var bootstrapCalls atomic.Int32
|
|
secondary.runtime.httpClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
bootstrapCalls.Add(1)
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK, Header: make(http.Header), Request: req,
|
|
Body: io.NopCloser(strings.NewReader(`{"auth_url":"https://auth.example.test/verify"}`)),
|
|
}, nil
|
|
})}
|
|
expectsVerification := mode == "fresh-response" || mode == "saved-challenge" || mode == "cached-unavailable" || mode == "cold-session"
|
|
if expectsVerification && mode != "cold-session" {
|
|
_, err := newExtensionProviderWrapper(secondary).SearchTracks("Song Artist", 1)
|
|
if err == nil || GetPendingAuthRequest(secondary.ID) == nil {
|
|
t.Fatalf("metadata lookup did not create a pending challenge: %v", err)
|
|
}
|
|
}
|
|
if !expectsVerification {
|
|
saveUsableSignedSession(t, secondary.runtime, *secondary.Manifest.SignedSession, "authenticated-session")
|
|
}
|
|
if err := last.ensureRuntimeReady(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var laterCalls atomic.Int32
|
|
if err := last.VM.Set("recordLastProvider", func() { laterCalls.Add(1) }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response, err := DownloadWithExtensionFallback(DownloadRequest{
|
|
Service: primary.ID, Source: primary.ID, ItemID: "fallback-" + mode,
|
|
SpotifyID: "source-track", TrackName: "Song", ArtistName: "Artist", AlbumName: "Album",
|
|
ISRC: "USABC2600001", ReleaseDate: "2026-01-01", Quality: "lossless",
|
|
OutputDir: t.TempDir(), FilenameFormat: "{title}", UseFallback: true,
|
|
})
|
|
if err != nil || response == nil {
|
|
t.Fatalf("fallback response=%+v error=%v", response, err)
|
|
}
|
|
wantType := "verification_required"
|
|
switch mode {
|
|
case "network-error":
|
|
wantType = "network"
|
|
case "download-error":
|
|
wantType = "script_error"
|
|
case "empty-download-error":
|
|
wantType = "extension_error"
|
|
}
|
|
if response.ErrorType != wantType || response.Service != secondary.ID || response.RetryAfterSeconds != 0 {
|
|
t.Fatalf("failure lost its type, owner, or retry delay: %+v", response)
|
|
}
|
|
if expectsVerification {
|
|
if laterCalls.Load() != 0 || bootstrapCalls.Load() != 1 {
|
|
t.Fatalf("pending challenge was skipped or recreated: later=%d bootstrap=%d", laterCalls.Load(), bootstrapCalls.Load())
|
|
}
|
|
} else if laterCalls.Load() != 1 || bootstrapCalls.Load() != 0 {
|
|
t.Fatalf("authenticated failures must allow fallback without bootstrapping: later=%d bootstrap=%d", laterCalls.Load(), bootstrapCalls.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|