mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-16 14:55:27 +02:00
fix(extensions): preserve verification on availability errors
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func TestAvailabilityPreservesCanonicalVerificationOnFailure(t *testing.T) {
|
||||
for _, script := range []string{
|
||||
`throw new Error("VERIFY_REQUIRED");`,
|
||||
`return null;`,
|
||||
`return undefined;`,
|
||||
`return {available:false};`,
|
||||
} {
|
||||
t.Run(script, func(t *testing.T) {
|
||||
ext := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
t.Cleanup(func() { teardownExtension(ext) })
|
||||
if err := ext.ensureRuntimeReady(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ext.VM.Set("requireChallenge", func(goja.FunctionCall) goja.Value {
|
||||
ext.runtime.noteVerificationRequired("https://example.test/challenge")
|
||||
return goja.Undefined()
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ext.VM.RunString(`extension.checkAvailability = function(){ requireChallenge(); ` + script + ` };`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := newExtensionProviderWrapper(ext).CheckAvailabilityForItemID("", "Song", "Artist", "", "", "", "", 180000, "")
|
||||
if err == nil || classifyDownloadErrorType(err.Error()) != "verification_required" || !strings.Contains(err.Error(), ext.ID) {
|
||||
t.Fatalf("canonical verification not preserved: %v", err)
|
||||
}
|
||||
if ext.runtime.consumeVerificationRequired() != "" {
|
||||
t.Fatal("verification evidence leaked into the next call")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityDoesNotPromoteUntrustedOrStaleVerification(t *testing.T) {
|
||||
ext := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
t.Cleanup(func() { teardownExtension(ext) })
|
||||
if err := ext.ensureRuntimeReady(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ext.VM.RunString(`extension.checkAvailability = function(){ throw new Error("VERIFY_REQUIRED"); };`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ext.runtime.noteVerificationRequired("https://example.test/stale-challenge")
|
||||
_, err := newExtensionProviderWrapper(ext).CheckAvailabilityForItemID("", "Song", "Artist", "", "", "", "", 180000, "")
|
||||
if err == nil || classifyDownloadErrorType(err.Error()) == "verification_required" {
|
||||
t.Fatalf("untrusted exception inherited a previous challenge: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -529,21 +529,47 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a
|
||||
availabilityOptions["track"] = trackContexts[0]
|
||||
}
|
||||
|
||||
var availabilityRuntime *extensionRuntime
|
||||
consumeVerificationError := func() error {
|
||||
if availabilityRuntime != nil && availabilityRuntime.consumeVerificationRequired() != "" {
|
||||
return fmt.Errorf(
|
||||
"verification_required: extension '%s' needs signed-session verification",
|
||||
p.extension.ID,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return callExtension(p, extCallOpts{
|
||||
perfName: "checkAvailability",
|
||||
invoke: extensionMethodInvocation("checkAvailability", isrc, trackName, artistName, availabilityOptions),
|
||||
timeout: DefaultJSTimeout,
|
||||
itemID: itemID,
|
||||
invoke: func(vm *goja.Runtime) (goja.Value, error) {
|
||||
result, err := invokeExtensionMethod(vm, "checkAvailability", isrc, trackName, artistName, availabilityOptions)
|
||||
// A thrown JS error must preserve the same canonical runtime evidence
|
||||
// as a returned unavailable result. Cancellation and timeout still
|
||||
// take precedence in callExtension.
|
||||
if err != nil {
|
||||
if verificationErr := consumeVerificationError(); verificationErr != nil {
|
||||
return nil, verificationErr
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
},
|
||||
timeout: DefaultJSTimeout,
|
||||
itemID: itemID,
|
||||
beforeRun: func() func() {
|
||||
// Drop any stale flag so the post-run check below only sees
|
||||
// verification requested by THIS call.
|
||||
if p.extension.runtime != nil {
|
||||
p.extension.runtime.consumeVerificationRequired()
|
||||
availabilityRuntime = p.extension.runtime
|
||||
if availabilityRuntime != nil {
|
||||
availabilityRuntime.consumeVerificationRequired()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, func(perf *extensionCallPerf, result goja.Value) (*ExtAvailabilityResult, error) {
|
||||
if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
|
||||
if err := consumeVerificationError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ExtAvailabilityResult{Available: false, Reason: "not implemented"}, nil
|
||||
}
|
||||
parseStartedAt := time.Now()
|
||||
@@ -555,12 +581,9 @@ func (p *extensionProviderWrapper) CheckAvailabilityForItemID(isrc, trackName, a
|
||||
// "not available", which would silently skip this provider's
|
||||
// challenge; surface it as an error so the fallback loop pauses and
|
||||
// opens the challenge instead.
|
||||
if !availability.Available && p.extension.runtime != nil {
|
||||
if p.extension.runtime.consumeVerificationRequired() != "" {
|
||||
return nil, fmt.Errorf(
|
||||
"verification_required: extension '%s' needs signed-session verification",
|
||||
p.extension.ID,
|
||||
)
|
||||
if !availability.Available {
|
||||
if err := consumeVerificationError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &availability, nil
|
||||
|
||||
Reference in New Issue
Block a user