mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-15 05:10:28 +02:00
5bdaa35ced
- 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
306 lines
11 KiB
Go
306 lines
11 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/go-flac/flacvorbis/v2"
|
|
)
|
|
|
|
func TestReadFileMetadataAndCueLibraryWrappers(t *testing.T) {
|
|
dir := t.TempDir()
|
|
mp3Path := filepath.Join(dir, "tagged.mp3")
|
|
tag := buildID3v23Tag(
|
|
id3TextFrame("TIT2", "Title"),
|
|
id3TextFrame("TPE1", "Artist"),
|
|
id3TextFrame("TALB", "Album"),
|
|
id3TextFrame("TRCK", "4/12"),
|
|
id3CommentFrame("USLT", "[00:00.00]Lyric"),
|
|
)
|
|
if err := os.WriteFile(mp3Path, append(tag, []byte{0xFF, 0xFB, 0x90, 0x64, 0, 0, 0, 0}...), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if jsonText, err := ReadFileMetadata(mp3Path); err != nil || !strings.Contains(jsonText, `"title":"Title"`) {
|
|
t.Fatalf("ReadFileMetadata mp3 = %q/%v", jsonText, err)
|
|
}
|
|
|
|
m4aPath := filepath.Join(dir, "tagged.m4a")
|
|
ilst := buildM4ATextTag("\xa9nam", "M4A Title")
|
|
if err := os.WriteFile(m4aPath, buildM4AFileWithIlst(ilst, true), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if jsonText, err := ReadFileMetadata(m4aPath); err != nil || !strings.Contains(jsonText, "M4A Title") {
|
|
t.Fatalf("ReadFileMetadata m4a = %q/%v", jsonText, err)
|
|
}
|
|
|
|
cuePath, _ := writeExportCueFixture(t, dir)
|
|
results, err := ScanCueFileForLibrary(cuePath, time.Now().Format(time.RFC3339))
|
|
if err != nil || len(results) != 1 || results[0].TrackName != "Song" {
|
|
t.Fatalf("ScanCueFileForLibrary = %#v/%v", results, err)
|
|
}
|
|
if _, err := ReadFileMetadata(filepath.Join(dir, "unsupported.txt")); err == nil {
|
|
t.Fatal("expected unsupported metadata format")
|
|
}
|
|
}
|
|
|
|
func TestOutputFDFilePathBranches(t *testing.T) {
|
|
dir := t.TempDir()
|
|
outputPath := filepath.Join(dir, "out.bin")
|
|
file, err := openOutputForWrite(outputPath, 0)
|
|
if err != nil {
|
|
t.Fatalf("openOutputForWrite path: %v", err)
|
|
}
|
|
if _, err := file.Write([]byte("data")); err != nil {
|
|
t.Fatalf("write output: %v", err)
|
|
}
|
|
file.Close()
|
|
if !isFDOutput(1) || isFDOutput(0) {
|
|
t.Fatal("isFDOutput mismatch")
|
|
}
|
|
closeOwnedOutputFD(0)
|
|
if err := prepareDupFDForWrite(11, 10); err != nil {
|
|
t.Fatalf("prepareDupFDForWrite: %v", err)
|
|
}
|
|
closeOwnedOutputFD(11)
|
|
cleanupOutputOnError(outputPath, 0)
|
|
if _, err := os.Stat(outputPath); !os.IsNotExist(err) {
|
|
t.Fatalf("cleanup should remove output path, stat err=%v", err)
|
|
}
|
|
cleanupOutputOnError("", 0)
|
|
cleanupOutputOnError("/proc/self/fd/1", 0)
|
|
cleanupOutputOnError(filepath.Join(dir, "kept.bin"), 10)
|
|
}
|
|
|
|
func TestMoreSmallConstructorsRuntimeAndMetadataHelpers(t *testing.T) {
|
|
if cfg := DefaultRetryConfig(); cfg.MaxRetries == 0 || cfg.BackoffFactor <= 1 {
|
|
t.Fatalf("DefaultRetryConfig = %#v", cfg)
|
|
}
|
|
if NewAppleMusicClient().httpClient == nil || NewNeteaseClient().httpClient == nil || NewMusixmatchClient().httpClient == nil || NewQQMusicClient().httpClient == nil {
|
|
t.Fatal("expected lyric provider HTTP clients")
|
|
}
|
|
if NewIDHSClient().client == nil {
|
|
t.Fatal("expected IDHS HTTP client")
|
|
}
|
|
ClearTrackCache()
|
|
|
|
vm := goja.New()
|
|
runtime := &extensionRuntime{extensionID: "misc-runtime", vm: vm, settings: map[string]interface{}{}}
|
|
if parseExtensionTimeoutSeconds(" 42 ") != 42 || parseExtensionTimeoutSeconds("bad") != 0 || parseExtensionTimeoutSeconds(float64(7)) != 7 {
|
|
t.Fatal("parseExtensionTimeoutSeconds mismatch")
|
|
}
|
|
if (&RedirectBlockedError{Domain: "blocked.example"}).Error() == "" || (&RedirectBlockedError{IsPrivate: true}).Error() == "" {
|
|
t.Fatal("RedirectBlockedError Error mismatch")
|
|
}
|
|
runtime.SetSettings(map[string]interface{}{"quality": "lossless"})
|
|
if runtime.settings["quality"] != "lossless" {
|
|
t.Fatal("SetSettings mismatch")
|
|
}
|
|
jar, _ := newSimpleCookieJar()
|
|
cookieURL, _ := url.Parse("https://example.test/")
|
|
jar.SetCookies(cookieURL, []*http.Cookie{{Name: "a", Value: "b"}})
|
|
if cookies := jar.Cookies(cookieURL); len(cookies) != 1 || cookies[0].Value != "b" {
|
|
t.Fatalf("cookies = %#v", cookies)
|
|
}
|
|
|
|
if result := runtime.ffmpegExecute(goja.FunctionCall{}).Export().(map[string]interface{}); result["success"] != false {
|
|
t.Fatalf("ffmpegExecute missing args = %#v", result)
|
|
}
|
|
if result := runtime.ffmpegGetInfo(goja.FunctionCall{}).Export().(map[string]interface{}); result["success"] != false {
|
|
t.Fatalf("ffmpegGetInfo missing args = %#v", result)
|
|
}
|
|
if result := runtime.ffmpegGetInfo(goja.FunctionCall{Arguments: []goja.Value{vm.ToValue("missing.flac")}}).Export().(map[string]interface{}); result["success"] != false {
|
|
t.Fatalf("ffmpegGetInfo missing file = %#v", result)
|
|
}
|
|
if result := runtime.ffmpegConvert(goja.FunctionCall{}).Export().(map[string]interface{}); result["success"] != false {
|
|
t.Fatalf("ffmpegConvert missing args = %#v", result)
|
|
}
|
|
|
|
cmt := flacvorbis.New()
|
|
setComment(cmt, "TITLE", "Song")
|
|
setComment(cmt, "ARTIST", "Artist")
|
|
if getComment(cmt, "TITLE") != "Song" || getJoinedComment(cmt, "ARTIST") != "Artist" {
|
|
t.Fatalf("comments = %#v", cmt.Comments)
|
|
}
|
|
setOrClearComment(cmt, "TITLE", "")
|
|
if getComment(cmt, "TITLE") != "" {
|
|
t.Fatal("setOrClearComment should remove empty value")
|
|
}
|
|
setOrClearArtistComments(cmt, "ARTIST", "A; B", artistTagModeSplitVorbis)
|
|
if joined := getJoinedComment(cmt, "ARTIST"); !strings.Contains(joined, "A") || !strings.Contains(joined, "B") {
|
|
t.Fatalf("split artist comments = %q", joined)
|
|
}
|
|
removeCommentKey(cmt, "ARTIST")
|
|
if getComment(cmt, "ARTIST") != "" {
|
|
t.Fatal("removeCommentKey failed")
|
|
}
|
|
if fileExists(filepath.Join(t.TempDir(), "missing")) {
|
|
t.Fatal("missing file should not exist")
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("cover"))
|
|
}))
|
|
defer server.Close()
|
|
SetNetworkCompatibilityOptions(true, false)
|
|
defer SetNetworkCompatibilityOptions(false, false)
|
|
coverPath := filepath.Join(t.TempDir(), "cover.jpg")
|
|
if err := DownloadCoverToFile(server.URL+"/cover.jpg", coverPath, false); err != nil {
|
|
t.Fatalf("DownloadCoverToFile: %v", err)
|
|
}
|
|
if string(mustReadFile(t, coverPath)) != "cover" {
|
|
t.Fatal("downloaded cover mismatch")
|
|
}
|
|
|
|
parallel := FetchCoverAndLyricsParallel(server.URL+"/cover.jpg", false, "spotify-1", "Song Instrumental", "Artist", true, 180000)
|
|
if string(parallel.CoverData) != "cover" || parallel.CoverErr != nil || parallel.LyricsErr == nil {
|
|
t.Fatalf("FetchCoverAndLyricsParallel = %#v", parallel)
|
|
}
|
|
emptyParallel := FetchCoverAndLyricsParallel("", false, "", "", "", false, 0)
|
|
if emptyParallel.CoverData != nil || emptyParallel.LyricsData != nil {
|
|
t.Fatalf("empty FetchCoverAndLyricsParallel = %#v", emptyParallel)
|
|
}
|
|
}
|
|
|
|
func TestExtensionHealthInitializeVMAndCustomSearchWrappers(t *testing.T) {
|
|
dir := t.TempDir()
|
|
extDir := filepath.Join(dir, "ext")
|
|
if err := os.MkdirAll(extDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(extDir, "index.js"), []byte(testExtensionJS), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ext := &loadedExtension{
|
|
ID: "health-ext",
|
|
Manifest: &ExtensionManifest{
|
|
Name: "health-ext",
|
|
DisplayName: "Health",
|
|
Version: "1.0.0",
|
|
Description: "Health extension",
|
|
Types: []ExtensionType{ExtensionTypeMetadataProvider},
|
|
SearchBehavior: &SearchBehaviorConfig{
|
|
Enabled: true,
|
|
Primary: true,
|
|
},
|
|
ServiceHealth: []ExtensionHealthCheck{{
|
|
ID: "bad",
|
|
URL: "http://health.example.test/status",
|
|
Required: true,
|
|
}},
|
|
},
|
|
Enabled: true,
|
|
SourceDir: extDir,
|
|
DataDir: filepath.Join(dir, "data"),
|
|
}
|
|
manager := getExtensionManager()
|
|
manager.mu.Lock()
|
|
if manager.extensions == nil {
|
|
manager.extensions = map[string]*loadedExtension{}
|
|
}
|
|
manager.extensions[ext.ID] = ext
|
|
manager.mu.Unlock()
|
|
defer func() {
|
|
manager.mu.Lock()
|
|
delete(manager.extensions, ext.ID)
|
|
manager.mu.Unlock()
|
|
}()
|
|
|
|
if err := manager.initializeVM(ext); err != nil {
|
|
t.Fatalf("initializeVM: %v", err)
|
|
}
|
|
if ext.VM == nil {
|
|
t.Fatal("expected initialized VM")
|
|
}
|
|
provider := &extensionProviderWrapper{extension: ext}
|
|
if tracks, err := provider.CustomSearch("needle", map[string]interface{}{"type": "track"}); err != nil || len(tracks) == 0 {
|
|
t.Fatalf("CustomSearch = %#v/%v", tracks, err)
|
|
}
|
|
cancelMu.Lock()
|
|
delete(cancelMap, "custom-item-unique")
|
|
cancelMu.Unlock()
|
|
if tracks, err := provider.CustomSearchForItemID("needle", nil, "custom-item-unique"); err != nil || len(tracks) == 0 {
|
|
t.Fatalf("CustomSearchForItemID = %#v/%v", tracks, err)
|
|
}
|
|
if healthJSON, err := CheckExtensionHealthJSON(ext.ID); err != nil || !strings.Contains(healthJSON, `"status":"offline"`) {
|
|
t.Fatalf("CheckExtensionHealthJSON = %q/%v", healthJSON, err)
|
|
}
|
|
teardownVMLocked(ext)
|
|
}
|
|
|
|
func TestManifestPerfMatchingAndTitleHelpers(t *testing.T) {
|
|
manifest := &ExtensionManifest{
|
|
Name: "misc-ext",
|
|
DisplayName: "Misc",
|
|
Version: "1.0.0",
|
|
Description: "Misc extension",
|
|
Types: []ExtensionType{ExtensionTypeMetadataProvider},
|
|
URLHandler: &URLHandlerConfig{Enabled: true, Patterns: []string{"example.test"}},
|
|
PostProcessing: &PostProcessingConfig{Hooks: []PostProcessingHook{{
|
|
ID: "hook", Name: "Hook",
|
|
}}},
|
|
}
|
|
data, err := manifest.ToJSON()
|
|
if err != nil || !strings.Contains(string(data), "misc-ext") {
|
|
t.Fatalf("ToJSON = %q/%v", string(data), err)
|
|
}
|
|
if !manifest.HasURLHandler() || !manifest.MatchesURL("https://example.test/track") || len(manifest.GetPostProcessingHooks()) != 1 {
|
|
t.Fatal("manifest helpers mismatch")
|
|
}
|
|
if (&ManifestValidationError{Field: "name", Message: "required"}).Error() == "" {
|
|
t.Fatal("manifest validation error string empty")
|
|
}
|
|
|
|
if extensionDurationMs(1500*time.Microsecond) != 1.5 {
|
|
t.Fatal("extensionDurationMs mismatch")
|
|
}
|
|
vm := goja.New()
|
|
value := vm.ToValue(map[string]interface{}{"tracks": []interface{}{1, 2, 3}})
|
|
if countExtensionTopLevelItems(vm, value) != 3 {
|
|
t.Fatal("countExtensionTopLevelItems mismatch")
|
|
}
|
|
if countExtensionTopLevelItems(vm, goja.Undefined()) != 0 {
|
|
t.Fatal("empty top-level item count mismatch")
|
|
}
|
|
|
|
if calculateStringSimilarity("", "") != 1 || calculateStringSimilarity("", "x") != 0 || levenshteinDistance("kitten", "sitting") != 3 {
|
|
t.Fatal("string similarity helpers mismatch")
|
|
}
|
|
var b strings.Builder
|
|
writeNormalizedArtistRune(&b, 'ß')
|
|
writeNormalizedArtistRune(&b, 'æ')
|
|
if b.String() != "ssae" {
|
|
t.Fatalf("writeNormalizedArtistRune = %q", b.String())
|
|
}
|
|
if !artistsMatch("Artist feat Guest", "Guest") || !sameWordsUnordered("B A", "A B") || !titlesMatch("Song (Remastered)", "Song") {
|
|
t.Fatal("artist/title matching mismatch")
|
|
}
|
|
if len(splitArtists("A & B, C x D")) != 4 {
|
|
t.Fatal("splitArtists mismatch")
|
|
}
|
|
if isLatinScript("東京") || !isLatinScript("Beyonce") {
|
|
t.Fatal("isLatinScript mismatch")
|
|
}
|
|
|
|
req := DownloadRequest{TrackName: "Song", ArtistName: "Artist", DurationMS: 180000}
|
|
if !trackMatchesRequest(req, resolvedTrackInfo{Title: "Song", ArtistName: "Artist", Duration: 181}, "test") {
|
|
t.Fatal("expected matching track")
|
|
}
|
|
if trackMatchesRequest(req, resolvedTrackInfo{Title: "Other", ArtistName: "Other", Duration: 240}, "test") {
|
|
t.Fatal("expected mismatching track")
|
|
}
|
|
|
|
var decoded map[string]interface{}
|
|
if err := json.Unmarshal(data, &decoded); err != nil || decoded["name"] != "misc-ext" {
|
|
t.Fatalf("manifest JSON decode = %#v/%v", decoded, err)
|
|
}
|
|
}
|