fix(extensions): preserve verification on availability errors

This commit is contained in:
zarzet
2026-09-06 16:03:29 +07:00
parent 0e71003e12
commit 4fc837b94c
2 changed files with 91 additions and 11 deletions
@@ -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)
}
}
+34 -11
View File
@@ -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