Files
SpotiFLAC-Mobile/go_backend/extension_fallback_verification_test.go
T

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())
}
})
}
}