SpotiFLAC-Mobile/go_backend/extension_runtime_supplement_test.go
zarzet 5bdaa35ced test: add comprehensive Go backend and Dart model test suites
- Add 16 Go supplement test files covering extension runtime, providers,
  health checks, lyrics, metadata, HTTP utils, library scan, and more
- Add Dart model/utils test suite (test/models_and_utils_test.dart)
- Update settings.g.dart with deduplicateDownloads serialization
2026-05-04 02:21:17 +07:00

747 lines
30 KiB
Go

package gobackend
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/dop251/goja"
)
func TestExtensionRuntimeAuthAndPolyfills(t *testing.T) {
vm := goja.New()
runtime := &extensionRuntime{
extensionID: "auth-ext",
manifest: &ExtensionManifest{
Name: "auth-ext",
Description: "Auth extension",
Version: "1.0.0",
Permissions: ExtensionPermissions{
Network: []string{"auth.example.com", "token.example.com", "api.example.com"},
},
},
settings: map[string]interface{}{},
httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
switch req.URL.Host {
case "token.example.com":
return &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"access_token":"access","refresh_token":"refresh","expires_in":3600}`)),
Request: req,
}, nil
case "api.example.com":
return &http.Response{
StatusCode: 200,
Header: http.Header{"X-Test": []string{"yes"}},
Body: io.NopCloser(strings.NewReader(`{"ok":true,"items":[1,2]}`)),
Request: req,
}, nil
default:
return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil
}
})},
vm: vm,
}
if err := validateExtensionAuthURL("https://user:pass@auth.example.com/login"); err == nil {
t.Fatal("expected embedded credential error")
}
if err := validateExtensionAuthURL("http://auth.example.com/login"); err == nil {
t.Fatal("expected non-https auth URL error")
}
if got := summarizeURLForLog("https://auth.example.com/login?token=secret"); got != "https://auth.example.com/login" {
t.Fatalf("summary = %q", got)
}
openResult := runtime.authOpenUrl(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("https://auth.example.com/login"),
vm.ToValue("app://callback"),
}}).Export().(map[string]interface{})
if openResult["success"] != true {
t.Fatalf("authOpenUrl = %#v", openResult)
}
if pending := GetPendingAuthRequest("auth-ext"); pending == nil || pending.AuthURL == "" {
t.Fatalf("pending auth = %#v", pending)
}
if code := runtime.authGetCode(goja.FunctionCall{}); !goja.IsUndefined(code) {
t.Fatalf("expected undefined code, got %v", code)
}
if ok := runtime.authSetCode(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(map[string]interface{}{"code": "abc", "access_token": "access", "refresh_token": "refresh", "expires_in": float64(60)})}}); !ok.ToBoolean() {
t.Fatal("authSetCode returned false")
}
if code := runtime.authGetCode(goja.FunctionCall{}).String(); code != "abc" {
t.Fatalf("code = %q", code)
}
if !runtime.authIsAuthenticated(goja.FunctionCall{}).ToBoolean() {
t.Fatal("expected authenticated runtime")
}
tokens := runtime.authGetTokens(goja.FunctionCall{}).Export().(map[string]interface{})
if tokens["access_token"] != "access" {
t.Fatalf("tokens = %#v", tokens)
}
pkce := runtime.authGeneratePKCE(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(float64(50))}}).Export().(map[string]interface{})
if pkce["method"] != "S256" || pkce["verifier"] == "" || pkce["challenge"] == "" {
t.Fatalf("pkce = %#v", pkce)
}
if current := runtime.authGetPKCE(goja.FunctionCall{}).Export().(map[string]interface{}); current["verifier"] == "" {
t.Fatalf("current pkce = %#v", current)
}
oauthConfig := map[string]interface{}{
"authUrl": "https://auth.example.com/oauth",
"clientId": "client",
"redirectUri": "app://callback",
"scope": "read",
"extraParams": map[string]interface{}{"prompt": "login"},
}
oauth := runtime.authStartOAuthWithPKCE(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(oauthConfig)}}).Export().(map[string]interface{})
if oauth["success"] != true || !strings.Contains(oauth["authUrl"].(string), "code_challenge") {
t.Fatalf("oauth = %#v", oauth)
}
tokenConfig := map[string]interface{}{
"tokenUrl": "https://token.example.com/token",
"clientId": "client",
"redirectUri": "app://callback",
"code": "abc",
}
token := runtime.authExchangeCodeWithPKCE(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(tokenConfig)}}).Export().(map[string]interface{})
if token["success"] != true || token["access_token"] != "access" {
t.Fatalf("token = %#v", token)
}
runtime.registerTextEncoderDecoder(vm)
runtime.registerURLClass(vm)
runtime.registerJSONGlobal(vm)
vm.Set("fetch", func(call goja.FunctionCall) goja.Value {
return runtime.fetchPolyfill(call)
})
vm.Set("atob", func(call goja.FunctionCall) goja.Value {
return runtime.atobPolyfill(call)
})
vm.Set("btoa", func(call goja.FunctionCall) goja.Value {
return runtime.btoaPolyfill(call)
})
value, err := vm.RunString(`
var encoded = btoa("hello");
var decoded = atob(encoded);
var te = new TextEncoder();
var bytes = te.encode("hi");
var into = te.encodeInto("hi", []);
var td = new TextDecoder();
var text = td.decode(bytes);
var url = new URL("/path?a=1&a=2#frag", "https://api.example.com/base");
var params = new URLSearchParams("?x=1");
params.append("x", "2");
params.set("y", "3");
var response = fetch("https://api.example.com/data", {method: "POST", body: {q: "x"}, headers: {"X-Client": "test"}});
JSON.stringify({
encoded: encoded,
decoded: decoded,
text: text,
read: into.read,
host: url.hostname,
first: url.searchParams.get("a"),
all: url.searchParams.getAll("a").length,
params: params.toString(),
ok: response.ok,
status: response.status,
jsonOk: response.json().ok,
bufferLen: response.arrayBuffer().length
});
`)
if err != nil {
t.Fatalf("polyfill script: %v", err)
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(value.String()), &result); err != nil {
t.Fatalf("decode polyfill result: %v", err)
}
if result["decoded"] != "hello" || result["host"] != "api.example.com" || result["ok"] != true {
t.Fatalf("polyfill result = %#v", result)
}
blocked := runtime.fetchPolyfill(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("https://blocked.example.com")}}).ToObject(vm)
if blocked.Get("ok").ToBoolean() {
t.Fatal("expected blocked fetch")
}
runtime.authClear(goja.FunctionCall{})
if runtime.authIsAuthenticated(goja.FunctionCall{}).ToBoolean() {
t.Fatal("expected auth cleared")
}
}
func TestExtensionStoreSettingsAndRuntimeStorage(t *testing.T) {
dir := t.TempDir()
store := &extensionStore{
registryURL: "https://registry.example.com/registry.json",
cacheDir: dir,
cacheTTL: time.Hour,
cache: &storeRegistry{
Version: 1,
UpdatedAt: "2026-05-04",
Extensions: []storeExtension{
{
ID: "coverage-ext",
Name: "coverage-ext",
DisplayNameAlt: "Coverage Extension",
Version: "2.0.0",
Description: "Metadata and lyrics provider",
DownloadURLAlt: "https://registry.example.com/coverage.spotiflac-ext",
IconURLAlt: "https://registry.example.com/icon.png",
Category: CategoryMetadata,
Tags: []string{"metadata", "lyrics"},
Downloads: 10,
UpdatedAt: "2026-05-04",
MinAppVersionAlt: "4.5.0",
},
{
ID: "utility-ext",
Name: "utility-ext",
Version: "1.0.0",
Description: "Utility",
DownloadURL: "https://registry.example.com/utility.spotiflac-ext",
Category: CategoryUtility,
UpdatedAt: "2026-05-04",
},
},
},
cacheTime: time.Now(),
}
store.saveDiskCache()
loadedStore := &extensionStore{cacheDir: dir}
loadedStore.loadDiskCache()
if loadedStore.cache == nil || len(loadedStore.cache.Extensions) != 2 {
t.Fatalf("loaded cache = %#v", loadedStore.cache)
}
if got := store.getRegistryURL(); got != "https://registry.example.com/registry.json" {
t.Fatalf("registry URL = %q", got)
}
store.setRegistryURL("https://registry.example.com/new.json")
if store.cache != nil {
t.Fatal("expected cache reset after registry URL change")
}
store.cache = loadedStore.cache
store.cacheTime = time.Now()
manager := getExtensionManager()
manager.mu.Lock()
if manager.extensions == nil {
manager.extensions = map[string]*loadedExtension{}
}
manager.extensions["coverage-ext"] = &loadedExtension{
ID: "coverage-ext",
Manifest: &ExtensionManifest{
Name: "coverage-ext",
DisplayName: "Coverage Extension",
Version: "1.0.0",
Description: "Installed",
Types: []ExtensionType{ExtensionTypeMetadataProvider},
},
Enabled: true,
}
manager.mu.Unlock()
defer func() {
manager.mu.Lock()
delete(manager.extensions, "coverage-ext")
manager.mu.Unlock()
}()
extensions, err := store.getExtensionsWithStatus(false)
if err != nil {
t.Fatalf("getExtensionsWithStatus: %v", err)
}
if len(extensions) != 2 || !extensions[0].IsInstalled || !extensions[0].HasUpdate {
t.Fatalf("extensions = %#v", extensions)
}
found, err := store.searchExtensions("lyrics", CategoryMetadata)
if err != nil || len(found) != 1 || found[0].ID != "coverage-ext" {
t.Fatalf("search = %#v/%v", found, err)
}
all, err := store.searchExtensions("", "")
if err != nil || len(all) != 2 {
t.Fatalf("all search = %#v/%v", all, err)
}
if cats := store.getCategories(); len(cats) != 5 {
t.Fatalf("categories = %#v", cats)
}
if !containsIgnoreCase("Hello Metadata", "metadata") || findSubstring("abcdef", "cd") != 2 || containsStr("abc", "z") {
t.Fatal("string helper mismatch")
}
if err := requireHTTPSURL("http://example.com", "registry"); err == nil {
t.Fatal("expected HTTPS validation error")
}
if _, err := resolveRegistryURL(""); err == nil {
t.Fatal("expected empty registry URL error")
}
if resolved, err := resolveRegistryURL("http://github.com/owner/repo"); err != nil || !strings.Contains(resolved, "raw.githubusercontent.com/owner/repo") {
t.Fatalf("resolved registry = %q/%v", resolved, err)
}
store.clearCache()
if store.cache != nil {
t.Fatal("expected cleared store cache")
}
settingsStore := &ExtensionSettingsStore{settings: map[string]map[string]interface{}{}}
if err := settingsStore.SetDataDir(filepath.Join(dir, "settings")); err != nil {
t.Fatalf("SetDataDir: %v", err)
}
if err := settingsStore.Set("ext", "quality", "lossless"); err != nil {
t.Fatalf("settings Set: %v", err)
}
if value, err := settingsStore.Get("ext", "quality"); err != nil || value != "lossless" {
t.Fatalf("settings Get = %#v/%v", value, err)
}
if _, err := settingsStore.Get("ext", "missing"); err == nil {
t.Fatal("expected missing setting error")
}
if err := settingsStore.SetAll("ext", map[string]interface{}{"a": float64(1), "_secret": "hidden"}); err != nil {
t.Fatalf("settings SetAll: %v", err)
}
if all := settingsStore.GetAll("ext"); all["a"] != float64(1) {
t.Fatalf("settings all = %#v", all)
}
if err := settingsStore.Remove("ext", "a"); err != nil {
t.Fatalf("settings Remove: %v", err)
}
if err := settingsStore.RemoveAll("ext"); err != nil {
t.Fatalf("settings RemoveAll: %v", err)
}
if jsonText, err := settingsStore.GetAllExtensionSettingsJSON(); err != nil || jsonText == "" {
t.Fatalf("settings JSON = %q/%v", jsonText, err)
}
reloaded := &ExtensionSettingsStore{settings: map[string]map[string]interface{}{}}
if err := reloaded.SetDataDir(settingsStore.dataDir); err != nil {
t.Fatalf("reload settings: %v", err)
}
vm := goja.New()
runtime := &extensionRuntime{
extensionID: "storage-ext",
dataDir: filepath.Join(dir, "runtime"),
vm: vm,
storageFlushDelay: time.Hour,
}
if err := os.MkdirAll(runtime.dataDir, 0755); err != nil {
t.Fatal(err)
}
if got := runtime.storageGet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("missing"), vm.ToValue("fallback")}}).String(); got != "fallback" {
t.Fatalf("storage fallback = %q", got)
}
if ok := runtime.storageSet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("key"), vm.ToValue(map[string]interface{}{"nested": "value"})}}); !ok.ToBoolean() {
t.Fatal("storageSet false")
}
if ok := runtime.storageSet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("key"), vm.ToValue(map[string]interface{}{"nested": "value"})}}); !ok.ToBoolean() {
t.Fatal("storageSet equal false")
}
loaded, err := runtime.loadStorage()
if err != nil || loaded["key"] == nil {
t.Fatalf("loadStorage = %#v/%v", loaded, err)
}
if err := runtime.flushStorageNow(); err != nil {
t.Fatalf("flushStorageNow: %v", err)
}
if ok := runtime.storageRemove(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("key")}}); !ok.ToBoolean() {
t.Fatal("storageRemove false")
}
runtime.closeStorageFlusher()
if ok := runtime.storageSet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("after_close"), vm.ToValue("x")}}); ok.ToBoolean() {
t.Fatal("expected storageSet false after close")
}
credRuntime := &extensionRuntime{
extensionID: "cred-ext",
dataDir: filepath.Join(dir, "creds"),
vm: vm,
}
if err := os.MkdirAll(credRuntime.dataDir, 0755); err != nil {
t.Fatal(err)
}
if result := credRuntime.credentialsStore(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("token"), vm.ToValue("secret")}}).Export().(map[string]interface{}); result["success"] != true {
t.Fatalf("credentialsStore = %#v", result)
}
if got := credRuntime.credentialsGet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("token")}}).String(); got != "secret" {
t.Fatalf("credential = %q", got)
}
if !credRuntime.credentialsHas(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("token")}}).ToBoolean() {
t.Fatal("expected credential")
}
if ok := credRuntime.credentialsRemove(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("token")}}); !ok.ToBoolean() {
t.Fatal("credentialsRemove false")
}
if credRuntime.credentialsHas(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("token")}}).ToBoolean() {
t.Fatal("expected credential removed")
}
key, err := credRuntime.getEncryptionKey()
if err != nil {
t.Fatalf("getEncryptionKey: %v", err)
}
encrypted, err := encryptAES([]byte("plain"), key)
if err != nil {
t.Fatalf("encryptAES: %v", err)
}
decrypted, err := decryptAES(encrypted, key)
if err != nil || string(decrypted) != "plain" {
t.Fatalf("decryptAES = %q/%v", decrypted, err)
}
if _, err := decryptAES([]byte("short"), key); err == nil {
t.Fatal("expected short ciphertext error")
}
}
func TestExtensionRuntimeHTTPMatchingAndMetadataHelpers(t *testing.T) {
vm := goja.New()
jar, _ := newSimpleCookieJar()
runtime := &extensionRuntime{
extensionID: "http-ext",
manifest: &ExtensionManifest{
Name: "http-ext",
Description: "HTTP extension",
Version: "1.0.0",
Permissions: ExtensionPermissions{
Network: []string{"api.example.com"},
},
},
vm: vm,
cookieJar: jar,
httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
var body []byte
if req.Body != nil {
body, _ = io.ReadAll(req.Body)
}
header := make(http.Header)
header.Set("X-Method", req.Method)
if req.URL.Path == "/huge" {
return &http.Response{StatusCode: 200, Header: header, Body: io.NopCloser(io.LimitReader(strings.NewReader(strings.Repeat("x", maxExtensionHTTPResponseBytes+2)), maxExtensionHTTPResponseBytes+2)), Request: req}, nil
}
return &http.Response{
StatusCode: 201,
Header: header,
Body: io.NopCloser(strings.NewReader(req.Method + ":" + string(body))),
Request: req,
}, nil
})},
}
if err := runtime.validateDomain("https://api.example.com/path"); err != nil {
t.Fatalf("validateDomain allowed: %v", err)
}
for _, rawURL := range []string{"notaurl", "http://api.example.com", "https://user:pass@api.example.com", "https://127.0.0.1/x", "https://blocked.example.com/x"} {
if err := runtime.validateDomain(rawURL); err == nil {
t.Fatalf("expected domain validation error for %s", rawURL)
}
}
if got := runtime.httpGet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("https://api.example.com/get"), vm.ToValue(map[string]interface{}{"X-Test": "yes"})}}).Export().(map[string]interface{}); got["status"] != 201 || !strings.Contains(got["body"].(string), "GET") {
t.Fatalf("httpGet = %#v", got)
}
if got := runtime.httpPost(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("https://api.example.com/post"), vm.ToValue(map[string]interface{}{"a": "b"})}}).Export().(map[string]interface{}); !strings.Contains(got["body"].(string), "POST") {
t.Fatalf("httpPost = %#v", got)
}
requestOptions := map[string]interface{}{"method": "patch", "body": []interface{}{"x"}, "headers": map[string]interface{}{"X-Req": "1"}}
if got := runtime.httpRequest(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("https://api.example.com/request"), vm.ToValue(requestOptions)}}).Export().(map[string]interface{}); !strings.Contains(got["body"].(string), "PATCH") {
t.Fatalf("httpRequest = %#v", got)
}
for _, method := range []struct {
name string
call func(goja.FunctionCall) goja.Value
args []goja.Value
}{
{name: "PUT", call: runtime.httpPut, args: []goja.Value{vm.ToValue("https://api.example.com/put"), vm.ToValue("body")}},
{name: "DELETE", call: runtime.httpDelete, args: []goja.Value{vm.ToValue("https://api.example.com/delete"), vm.ToValue(map[string]interface{}{"X-Delete": "1"})}},
{name: "PATCH", call: runtime.httpPatch, args: []goja.Value{vm.ToValue("https://api.example.com/patch"), vm.ToValue(map[string]interface{}{"p": "q"})}},
} {
if got := method.call(goja.FunctionCall{Arguments: method.args}).Export().(map[string]interface{}); !strings.Contains(got["body"].(string), method.name) {
t.Fatalf("%s = %#v", method.name, got)
}
}
if got := runtime.httpGet(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("https://api.example.com/huge")}}).Export().(map[string]interface{}); !strings.Contains(got["error"].(string), "exceeds") {
t.Fatalf("huge response = %#v", got)
}
if !runtime.httpClearCookies(goja.FunctionCall{}).ToBoolean() {
t.Fatal("expected cookies cleared")
}
if runtime.matchingCompareStrings(goja.FunctionCall{}).ToFloat() != 0 {
t.Fatal("missing string compare args should be zero")
}
if runtime.matchingCompareStrings(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("Song"), vm.ToValue("song")}}).ToFloat() != 1 {
t.Fatal("expected exact string similarity")
}
if runtime.matchingCompareDuration(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(180000), vm.ToValue(182000)}}).ToBoolean() != true {
t.Fatal("expected duration match")
}
if runtime.matchingNormalizeString(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("Song (Remastered) feat. Guest!")}}).String() != "song" {
t.Fatalf("normalized = %q", runtime.matchingNormalizeString(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("Song (Remastered) feat. Guest!")}}).String())
}
if formatMusicBrainzGenre([]musicBrainzTag{{Count: 1, Name: "rock"}, {Count: 5, Name: "electronic"}, {Count: 10, Name: "rock"}}) != "Electronic" {
t.Fatal("unexpected genre selection")
}
credits := []musicBrainzArtistCredit{{Name: "A", JoinPhrase: " & "}, {Name: "B"}}
if formatMusicBrainzArtistCredit(credits) != "A & B" {
t.Fatal("artist credit format mismatch")
}
releases := []musicBrainzRelease{
{Title: "Other", ArtistCredit: []musicBrainzArtistCredit{{Name: "Fallback"}}},
{Title: "Album", ArtistCredit: credits},
}
if selectMusicBrainzAlbumArtist(releases, "Album") != "A & B" || selectMusicBrainzAlbumArtist(releases, "") != "Fallback" {
t.Fatal("album artist selection mismatch")
}
}
func TestExtensionRuntimeFileAPIs(t *testing.T) {
vm := goja.New()
dir := t.TempDir()
SetAllowedDownloadDirs(nil)
defer SetAllowedDownloadDirs(nil)
fileBody := "chunk"
runtime := &extensionRuntime{
extensionID: "file-ext",
manifest: &ExtensionManifest{
Name: "file-ext",
Description: "File extension",
Version: "1.0.0",
Permissions: ExtensionPermissions{
File: true,
Network: []string{"files.example.com"},
},
},
dataDir: dir,
vm: vm,
httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
if req.Header.Get("Range") == "" {
body := "downloaded"
return &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
ContentLength: int64(len(body)),
Request: req,
}, nil
}
rangeHeader := req.Header.Get("Range")
start, end := 0, len(fileBody)-1
if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil {
start, end = 0, 1
}
if start < 0 {
start = 0
}
if end >= len(fileBody) {
end = len(fileBody) - 1
}
if start > len(fileBody) {
start = len(fileBody)
}
body := fileBody[start : end+1]
header := http.Header{"Content-Range": []string{fmt.Sprintf("bytes %d-%d/%d", start, end, len(fileBody))}}
return &http.Response{StatusCode: 206, Header: header, Body: io.NopCloser(strings.NewReader(body)), Request: req}, nil
})},
}
runtime.downloadClient = runtime.httpClient
if _, err := (&extensionRuntime{manifest: &ExtensionManifest{}}).validatePath("x"); err == nil {
t.Fatal("expected file permission error")
}
if _, err := runtime.validatePath("../escape.txt"); err == nil {
t.Fatal("expected sandbox escape error")
}
AddAllowedDownloadDir(dir)
absolutePath := filepath.Join(dir, "allowed.txt")
if got, err := runtime.validatePath(absolutePath); err != nil || got != absolutePath {
t.Fatalf("absolute validatePath = %q/%v", got, err)
}
write := runtime.fileWrite(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/a.txt"), vm.ToValue("hello")}}).Export().(map[string]interface{})
if write["success"] != true {
t.Fatalf("fileWrite = %#v", write)
}
if !runtime.fileExists(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/a.txt")}}).ToBoolean() {
t.Fatal("expected written file to exist")
}
read := runtime.fileRead(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/a.txt")}}).Export().(map[string]interface{})
if read["data"] != "hello" {
t.Fatalf("fileRead = %#v", read)
}
writeBytes := runtime.fileWriteBytes(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("nested/bytes.bin"),
vm.ToValue("4869"),
vm.ToValue(map[string]interface{}{"encoding": "hex", "truncate": true}),
}}).Export().(map[string]interface{})
if writeBytes["success"] != true {
t.Fatalf("fileWriteBytes = %#v", writeBytes)
}
appendBytes := runtime.fileWriteBytes(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("nested/bytes.bin"),
vm.ToValue([]interface{}{float64('!')}),
vm.ToValue(map[string]interface{}{"append": true}),
}}).Export().(map[string]interface{})
if appendBytes["success"] != true {
t.Fatalf("append fileWriteBytes = %#v", appendBytes)
}
readBytes := runtime.fileReadBytes(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("nested/bytes.bin"),
vm.ToValue(map[string]interface{}{"encoding": "text", "offset": float64(1), "length": float64(2)}),
}}).Export().(map[string]interface{})
if readBytes["data"] != "i!" || readBytes["bytes_read"] != 2 {
t.Fatalf("fileReadBytes = %#v", readBytes)
}
if bad := runtime.fileWriteBytes(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("nested/bad.bin"),
vm.ToValue("x"),
vm.ToValue(map[string]interface{}{"append": true, "offset": float64(1)}),
}}).Export().(map[string]interface{}); bad["success"] != false {
t.Fatalf("expected append+offset failure, got %#v", bad)
}
if bad := runtime.fileReadBytes(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("nested/bytes.bin"),
vm.ToValue(map[string]interface{}{"encoding": "bad"}),
}}).Export().(map[string]interface{}); bad["success"] != false {
t.Fatalf("expected bad encoding failure, got %#v", bad)
}
copyResult := runtime.fileCopy(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/bytes.bin"), vm.ToValue("nested/copy.bin")}}).Export().(map[string]interface{})
if copyResult["success"] != true {
t.Fatalf("fileCopy = %#v", copyResult)
}
moveResult := runtime.fileMove(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/copy.bin"), vm.ToValue("nested/moved.bin")}}).Export().(map[string]interface{})
if moveResult["success"] != true {
t.Fatalf("fileMove = %#v", moveResult)
}
sizeResult := runtime.fileGetSize(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/moved.bin")}}).Export().(map[string]interface{})
if sizeResult["success"] != true || sizeResult["size"] != int64(3) {
t.Fatalf("fileGetSize = %#v", sizeResult)
}
deleteResult := runtime.fileDelete(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("nested/moved.bin")}}).Export().(map[string]interface{})
if deleteResult["success"] != true {
t.Fatalf("fileDelete = %#v", deleteResult)
}
download := runtime.fileDownload(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("https://files.example.com/file"),
vm.ToValue("downloads/file.bin"),
}}).Export().(map[string]interface{})
if download["success"] != true {
t.Fatalf("fileDownload = %#v", download)
}
if data, err := os.ReadFile(filepath.Join(dir, "downloads/file.bin")); err != nil || string(data) != "downloaded" {
t.Fatalf("downloaded data = %q/%v", data, err)
}
chunked := runtime.fileDownload(goja.FunctionCall{Arguments: []goja.Value{
vm.ToValue("https://files.example.com/chunk"),
vm.ToValue("downloads/chunk.bin"),
vm.ToValue(map[string]interface{}{"chunked": float64(2), "headers": map[string]interface{}{"X-Test": "yes"}}),
}}).Export().(map[string]interface{})
if chunked["success"] != true {
t.Fatalf("chunked fileDownload = %#v", chunked)
}
if data, err := os.ReadFile(filepath.Join(dir, "downloads/chunk.bin")); err != nil || string(data) != fileBody {
t.Fatalf("chunked data = %q/%v", data, err)
}
if missing := runtime.fileDownload(goja.FunctionCall{}).Export().(map[string]interface{}); missing["success"] != false {
t.Fatalf("expected missing download args error, got %#v", missing)
}
}
func TestExtensionRuntimeUtilityAPIs(t *testing.T) {
vm := goja.New()
runtime := &extensionRuntime{extensionID: "utils-ext", vm: vm}
if runtime.sha256Hash(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("abc")}}).String() == "" {
t.Fatal("expected sha256")
}
if runtime.hmacSHA256(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("msg"), vm.ToValue("key")}}).String() == "" {
t.Fatal("expected hmac sha256")
}
if runtime.hmacSHA256Base64(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("msg"), vm.ToValue("key")}}).String() == "" {
t.Fatal("expected hmac sha256 base64")
}
if value := runtime.hmacSHA1(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue([]interface{}{float64(1), float64(2)}), vm.ToValue([]interface{}{float64(3)})}}); len(value.Export().([]interface{})) == 0 {
t.Fatal("expected hmac sha1 bytes")
}
if !goja.IsUndefined(runtime.parseJSON(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(`{bad`)}})) {
t.Fatal("expected invalid JSON to return undefined")
}
parsed := runtime.parseJSON(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(`{"ok":true}`)}}).Export().(map[string]interface{})
if parsed["ok"] != true {
t.Fatalf("parseJSON = %#v", parsed)
}
if text := runtime.stringifyJSON(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(map[string]interface{}{"ok": true})}}).String(); !strings.Contains(text, "ok") {
t.Fatalf("stringifyJSON = %q", text)
}
encrypted := runtime.cryptoEncrypt(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("plain"), vm.ToValue("secret")}}).Export().(map[string]interface{})
if encrypted["success"] != true || encrypted["data"] == "" {
t.Fatalf("cryptoEncrypt = %#v", encrypted)
}
decrypted := runtime.cryptoDecrypt(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(encrypted["data"]), vm.ToValue("secret")}}).Export().(map[string]interface{})
if decrypted["success"] != true || decrypted["data"] != "plain" {
t.Fatalf("cryptoDecrypt = %#v", decrypted)
}
if bad := runtime.cryptoDecrypt(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("bad"), vm.ToValue("secret")}}).Export().(map[string]interface{}); bad["success"] != false {
t.Fatalf("expected bad decrypt failure, got %#v", bad)
}
key := runtime.cryptoGenerateKey(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(float64(8))}}).Export().(map[string]interface{})
if key["success"] != true || key["key"] == "" || key["hex"] == "" {
t.Fatalf("cryptoGenerateKey = %#v", key)
}
if runtime.randomUserAgent(goja.FunctionCall{}).String() == "" || runtime.appUserAgent(goja.FunctionCall{}).String() == "" {
t.Fatal("expected user agents")
}
SetAppVersion("9.9.9")
if runtime.appVersion(goja.FunctionCall{}).String() != "9.9.9" {
t.Fatal("appVersion mismatch")
}
if !runtime.sleep(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(float64(0))}}).ToBoolean() {
t.Fatal("zero sleep should succeed")
}
itemID := "utils-item"
runtime.setActiveDownloadItemID(itemID)
initDownloadCancel(itemID)
if runtime.isDownloadCancelled(goja.FunctionCall{}).ToBoolean() {
t.Fatal("item should not be cancelled yet")
}
runtime.setDownloadStatus(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue(itemProgressStatusDownloading)}})
cancelDownload(itemID)
if !runtime.isDownloadCancelled(goja.FunctionCall{}).ToBoolean() {
t.Fatal("item should be cancelled")
}
clearDownloadCancel(itemID)
runtime.clearActiveDownloadItemID()
requestID := "utils-request"
runtime.setActiveRequestID(requestID)
initExtensionRequestCancel(requestID)
if runtime.isRequestCancelled(goja.FunctionCall{}).ToBoolean() {
t.Fatal("request should not be cancelled yet")
}
cancelExtensionRequest(requestID)
if !runtime.isRequestCancelled(goja.FunctionCall{}).ToBoolean() {
t.Fatal("request should be cancelled")
}
clearExtensionRequestCancel(requestID)
runtime.clearActiveRequestID()
if msg := runtime.formatLogArgs([]goja.Value{vm.ToValue("a"), vm.ToValue(1)}); msg != "a 1" {
t.Fatalf("formatLogArgs = %q", msg)
}
runtime.logDebug(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("debug")}})
runtime.logInfo(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("info")}})
runtime.logWarn(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("warn")}})
runtime.logError(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("error")}})
if clean := runtime.sanitizeFilenameWrapper(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("A/B?")}}).String(); strings.ContainsAny(clean, "/?") {
t.Fatalf("sanitize wrapper = %q", clean)
}
}