package gobackend import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestExtensionManagerPackageLifecycle(t *testing.T) { dir := t.TempDir() extensionsDir := filepath.Join(dir, "extensions") dataDir := filepath.Join(dir, "data") manager := &extensionManager{extensions: map[string]*loadedExtension{}} if err := manager.SetDirectories(extensionsDir, dataDir); err != nil { t.Fatalf("SetDirectories: %v", err) } if err := GetExtensionSettingsStore().SetDataDir(dataDir); err != nil { t.Fatalf("settings data dir: %v", err) } js := ` var cleaned = false; registerExtension({ initialize: function(settings) { this.settings = settings || {}; }, cleanup: function() { cleaned = true; }, doAction: function() { return { message: "done", setting_updates: { quality: "lossless" } }; }, getHomeFeed: function() { return [{ id: "home", title: "Home" }]; }, getBrowseCategories: function() { return [{ id: "cat", title: "Category" }]; }, searchTracks: function() { return { tracks: [], total: 0 }; }, fetchLyrics: function() { return { syncType: "UNSYNCED", lines: [{ words: "hello" }] }; }, getDownloadUrl: function() { return { url: "https://example.test/a.flac" }; } }); ` pkgV1 := filepath.Join(dir, "manager-ext-v1.spotiflac-ext") createTestExtensionPackage(t, pkgV1, "manager-ext", "1.0.0", js, map[string]string{"../unsafe.txt": "skip"}) pkgV2 := filepath.Join(dir, "manager-ext-v2.spotiflac-ext") createTestExtensionPackage(t, pkgV2, "manager-ext", "1.1.0", js, nil) if compareVersions("v1.2.0", "1.1.9") <= 0 || compareVersions("1.0.0", "1.0") != 0 || compareVersions("1.0.0", "1.0.1") >= 0 { t.Fatal("compareVersions mismatch") } if _, err := manager.LoadExtensionFromFile(filepath.Join(dir, "bad.txt")); err == nil { t.Fatal("expected bad extension suffix error") } if _, err := manager.LoadExtensionFromFile(filepath.Join(dir, "missing.spotiflac-ext")); err == nil { t.Fatal("expected invalid package error") } ext, err := manager.LoadExtensionFromFile(pkgV1) if err != nil { t.Fatalf("LoadExtensionFromFile: %v", err) } if ext.ID != "manager-ext" || ext.Enabled || ext.SourceDir == "" { t.Fatalf("loaded extension = %#v", ext) } if _, err := os.Stat(filepath.Join(ext.SourceDir, "unsafe.txt")); err == nil { t.Fatal("unsafe archive path should not be extracted") } if _, err := manager.LoadExtensionFromFile(pkgV1); err == nil { t.Fatal("expected duplicate version error") } installedJSON, err := manager.GetInstalledExtensionsJSON() if err != nil || !strings.Contains(installedJSON, "manager-ext") || !strings.Contains(installedJSON, "icon_path") { t.Fatalf("GetInstalledExtensionsJSON = %q/%v", installedJSON, err) } var installed []map[string]interface{} if err := json.Unmarshal([]byte(installedJSON), &installed); err != nil || len(installed) != 1 { t.Fatalf("decode installed = %#v/%v", installed, err) } if err := GetExtensionSettingsStore().Set("manager-ext", "quality", "lossless"); err != nil { t.Fatalf("settings Set: %v", err) } if err := manager.SetExtensionEnabled("manager-ext", true); err != nil { t.Fatalf("enable extension: %v", err) } if !ext.Enabled || ext.VM == nil || !ext.initialized { t.Fatalf("enabled extension = %#v", ext) } if err := manager.InitializeExtension("manager-ext", map[string]interface{}{"quality": "hires"}); err != nil { t.Fatalf("InitializeExtension: %v", err) } action, err := manager.InvokeAction("manager-ext", "doAction") if err != nil || action["success"] != true || action["message"] != "done" { t.Fatalf("InvokeAction = %#v/%v", action, err) } if err := manager.CleanupExtension("manager-ext"); err != nil { t.Fatalf("CleanupExtension: %v", err) } if err := manager.SetExtensionEnabled("manager-ext", false); err != nil { t.Fatalf("disable extension: %v", err) } if ext.VM != nil || ext.initialized { t.Fatalf("expected VM teardown, got %#v", ext) } if _, err := manager.InvokeAction("manager-ext", "doAction"); err == nil { t.Fatal("expected disabled action error") } upgradeJSON, err := manager.CheckExtensionUpgradeJSON(pkgV2) if err != nil || !strings.Contains(upgradeJSON, `"can_upgrade":true`) { t.Fatalf("CheckExtensionUpgradeJSON = %q/%v", upgradeJSON, err) } upgraded, err := manager.UpgradeExtension(pkgV2) if err != nil { t.Fatalf("UpgradeExtension: %v", err) } if upgraded.Manifest.Version != "1.1.0" { t.Fatalf("upgraded = %#v", upgraded.Manifest) } if _, err := manager.UpgradeExtension(pkgV1); err == nil { t.Fatal("expected downgrade error") } if err := manager.RemoveExtension("manager-ext"); err != nil { t.Fatalf("RemoveExtension: %v", err) } if _, err := manager.GetExtension("manager-ext"); err == nil { t.Fatal("expected removed extension missing") } dirExt := filepath.Join(extensionsDir, "dir-ext") if err := os.MkdirAll(dirExt, 0755); err != nil { t.Fatal(err) } manifest := `{"name":"dir-ext","displayName":"dir-ext","version":"1.0.0","description":"Directory extension","type":["metadata_provider"],"permissions":{}}` if err := os.WriteFile(filepath.Join(dirExt, "manifest.json"), []byte(manifest), 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dirExt, "index.js"), []byte(`registerExtension({searchTracks:function(){return {tracks:[], total:0};}});`), 0600); err != nil { t.Fatal(err) } loaded, loadErrs := manager.LoadExtensionsFromDirectory(extensionsDir) if len(loadErrs) != 0 || len(loaded) != 1 || loaded[0] != "dir-ext" { t.Fatalf("LoadExtensionsFromDirectory = %#v/%#v", loaded, loadErrs) } manager.UnloadAllExtensions() if len(manager.GetAllExtensions()) != 0 { t.Fatal("expected all extensions unloaded") } }