test: decouple app fixtures from extension identities

This commit is contained in:
zarzet
2026-09-06 16:03:11 +07:00
parent ead2b9c0ab
commit 0e71003e12
23 changed files with 215 additions and 215 deletions
+5 -5
View File
@@ -63,11 +63,11 @@ function track(id) {
isrc: "USRC17607839",
itemType: "track",
albumType: "album",
tidalId: "tidal-1",
qobuzId: "qobuz-1",
deezerId: "deezer-1",
spotifyId: "spotify:track:1",
externalLinks: { tidal: "https://tidal.example/1" },
tidalId: "alternate-id-a",
qobuzId: "alternate-id-b",
deezerId: "alternate-id-c",
spotifyId: "source-track-1",
externalLinks: { provider: "https://provider.example/1" },
label: "Label",
copyright: "Copyright",
genre: "Pop",
+19 -19
View File
@@ -7,7 +7,7 @@ func TestCrossExtensionShareUsesAlbumCollectionItems(t *testing.T) {
Manifest: &ExtensionManifest{
Capabilities: map[string]any{
"shareUrlTemplates": map[string]any{
"album": "https://music.apple.com/us/album/{id}",
"album": "https://media.example/album/{id}",
},
},
},
@@ -25,7 +25,7 @@ func TestCrossExtensionShareUsesAlbumCollectionItems(t *testing.T) {
if best == nil {
t.Fatal("expected album collection item to match")
}
if url := resolveCollectionShareURL(ext, "album", best); url != "https://music.apple.com/us/album/1440783617" {
if url := resolveCollectionShareURL(ext, "album", best); url != "https://media.example/album/1440783617" {
t.Fatalf("album share URL = %q", url)
}
}
@@ -35,7 +35,7 @@ func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
Manifest: &ExtensionManifest{
Capabilities: map[string]any{
"shareUrlTemplates": map[string]any{
"artist": "https://music.youtube.com/browse/{id}",
"artist": "https://media.example/artist/{id}",
},
},
},
@@ -52,29 +52,29 @@ func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
if best == nil {
t.Fatal("expected artist collection item to match")
}
if url := resolveCollectionShareURL(ext, "artist", best); url != "https://music.youtube.com/browse/UCrPe3hLA51968GwxHSZ1llw" {
if url := resolveCollectionShareURL(ext, "artist", best); url != "https://media.example/artist/UCrPe3hLA51968GwxHSZ1llw" {
t.Fatalf("artist share URL = %q", url)
}
}
func TestCrossExtensionShareCacheKeyIsProviderOrderStable(t *testing.T) {
apple := &extensionProviderWrapper{
providerA := &extensionProviderWrapper{
extension: &loadedExtension{
ID: "apple",
SourceDir: "/extensions/apple",
Manifest: &ExtensionManifest{DisplayName: "Apple Music"},
ID: "provider-a",
SourceDir: "/extensions/provider-a",
Manifest: &ExtensionManifest{DisplayName: "Provider A"},
},
}
qobuz := &extensionProviderWrapper{
providerB := &extensionProviderWrapper{
extension: &loadedExtension{
ID: "qobuz",
SourceDir: "/extensions/qobuz",
Manifest: &ExtensionManifest{DisplayName: "Qobuz"},
ID: "provider-b",
SourceDir: "/extensions/provider-b",
Manifest: &ExtensionManifest{DisplayName: "Provider B"},
},
}
first := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{apple, qobuz})
second := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{qobuz, apple})
first := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "metadata-source", []*extensionProviderWrapper{providerA, providerB})
second := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "metadata-source", []*extensionProviderWrapper{providerB, providerA})
if first != second {
t.Fatalf("cache key should not depend on provider order:\n%s\n%s", first, second)
}
@@ -82,17 +82,17 @@ func TestCrossExtensionShareCacheKeyIsProviderOrderStable(t *testing.T) {
func TestCrossExtensionShareCacheableSkipsTransientErrors(t *testing.T) {
cacheable := []CrossExtensionShareResult{
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
{ExtensionID: "qobuz", Error: "album not found"},
{ExtensionID: "tidal", Error: "no results"},
{ExtensionID: "provider-a", Found: true, URL: "https://media.example/album/1"},
{ExtensionID: "provider-b", Error: "album not found"},
{ExtensionID: "provider-c", Error: "no results"},
}
if !crossExtensionShareResultsCacheable(cacheable) {
t.Fatal("expected found and deterministic not-found results to be cacheable")
}
transient := []CrossExtensionShareResult{
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
{ExtensionID: "qobuz", Error: "request failed: timeout"},
{ExtensionID: "provider-a", Found: true, URL: "https://media.example/album/1"},
{ExtensionID: "provider-b", Error: "request failed: timeout"},
}
if crossExtensionShareResultsCacheable(transient) {
t.Fatal("expected transient extension errors to skip cache")
+3 -3
View File
@@ -3,15 +3,15 @@ package gobackend
import "testing"
func TestBuildDownloadedFileCommentKeepsSourceComment(t *testing.T) {
const source = "https://music.apple.com/us/album/example/123"
got := buildDownloadedFileComment(source, "https://music.amazon.com/albums/example")
const source = "https://source.example/album/example/123"
got := buildDownloadedFileComment(source, "https://provider.example/albums/example")
if got != source {
t.Fatalf("comment = %q, want %q", got, source)
}
}
func TestBuildDownloadedFileCommentUsesProviderCommentWhenSourceIsEmpty(t *testing.T) {
const provider = "https://music.amazon.com/albums/example"
const provider = "https://provider.example/albums/example"
got := buildDownloadedFileComment("", provider)
if got != provider {
t.Fatalf("comment = %q, want %q", got, provider)
@@ -19,7 +19,7 @@ func TestPreparedDownloadRequestCache(t *testing.T) {
ItemID: "item-1",
Service: "provider-a",
Source: "source-a",
SpotifyID: "spotify-1",
SpotifyID: "source-track-1",
TrackName: "Track",
ArtistName: "Artist",
OutputDir: "/new/output",
@@ -33,7 +33,7 @@ func TestPreparedDownloadRequestCache(t *testing.T) {
prepared.ISRC = "USRC17607839"
prepared.AlbumName = "Resolved Album"
prepared.AlbumArtist = "Resolved Album Artist"
prepared.DeezerID = "deezer-1"
prepared.DeezerID = "alternate-track-1"
prepared.Genre = "Pop"
prepared.OutputDir = "/stale/output"
prepared.OutputPath = "/stale/output/old.flac"
@@ -73,7 +73,7 @@ func TestPreparedDownloadRequestCacheRejectsChangedTrackAndExpiry(t *testing.T)
req := DownloadRequest{
ItemID: "item-2",
Service: "provider-a",
SpotifyID: "spotify-2",
SpotifyID: "source-track-2",
TrackName: "Track",
ArtistName: "Artist",
}
@@ -81,7 +81,7 @@ func TestPreparedDownloadRequestCacheRejectsChangedTrackAndExpiry(t *testing.T)
cachePreparedDownloadRequest(key, req)
changed := req
changed.SpotifyID = "spotify-other"
changed.SpotifyID = "source-track-other"
if _, _, ok := takePreparedDownloadRequest(downloadPreparationKey(changed), changed); ok {
t.Fatal("changed track must not reuse another track's prepared metadata")
}
+11 -11
View File
@@ -51,7 +51,7 @@ func TestBuildDownloadSuccessResponsePrefersRequestedAlbumMetadata(t *testing.T)
resp := buildDownloadSuccessResponse(
req,
result,
"tidal",
"download-provider",
"ok",
"/tmp/test.flac",
false,
@@ -186,7 +186,7 @@ func TestBuildDownloadSuccessResponseReturnsResolvedProviderFilename(t *testing.
req := DownloadRequest{
TrackName: "Track",
ArtistName: "Artist",
DownloadProvider: "soundcloud",
DownloadProvider: "download-provider",
ProviderTrackID: "998877",
FilenameFormat: "{artist} - {title} [{isrc}] [{provider}-{provider_id}]",
OutputExt: ".flac",
@@ -199,13 +199,13 @@ func TestBuildDownloadSuccessResponseReturnsResolvedProviderFilename(t *testing.
resp := buildDownloadSuccessResponse(
req,
result,
"soundcloud",
"download-provider",
"ok",
"/proc/self/fd/10",
false,
)
want := "Artist - Track [USABC1234567] [soundcloud-998877].m4a"
want := "Artist - Track [USABC1234567] [download-provider-998877].m4a"
if resp.ResolvedFileName != want {
t.Fatalf("resolved filename = %q, want %q", resp.ResolvedFileName, want)
}
@@ -229,7 +229,7 @@ func TestBuildDownloadSuccessResponseNormalizesDecryptionDescriptor(t *testing.T
resp := buildDownloadSuccessResponse(
req,
result,
"amazon",
"download-provider",
"ok",
"/tmp/test.m4a",
false,
@@ -426,7 +426,7 @@ func TestEnrichExtraMetadataByISRCPrefersDeezerGenre(t *testing.T) {
func TestApplyReEnrichTrackMetadataPreservesExistingReleaseDateWhenCandidateMissing(t *testing.T) {
req := reEnrichRequest{
SpotifyID: "spotify-track-id",
SpotifyID: "source-track-id",
AlbumName: "Original Album",
ReleaseDate: "2024-01-01",
ISRC: "REQ123",
@@ -541,7 +541,7 @@ func TestSelectBestReEnrichTrackPrefersCandidateWithReleaseDate(t *testing.T) {
AlbumName: "Album Name",
DurationMS: 180000,
ReleaseDate: "",
ProviderID: "spotify",
ProviderID: "metadata-a",
},
{
ID: "second",
@@ -550,7 +550,7 @@ func TestSelectBestReEnrichTrackPrefersCandidateWithReleaseDate(t *testing.T) {
AlbumName: "Album Name",
DurationMS: 180000,
ReleaseDate: "2024-03-09",
ProviderID: "deezer",
ProviderID: "metadata-b",
},
}
@@ -582,7 +582,7 @@ func TestSelectBestReEnrichTrackRejectsMismatchedSearchResults(t *testing.T) {
TrackNumber: 4,
DiscNumber: 1,
ISRC: "WRONG1234567",
ProviderID: "deezer",
ProviderID: "metadata-b",
},
}
@@ -606,7 +606,7 @@ func TestSelectBestReEnrichTrackAllowsExactISRCDespiteMetadataMismatch(t *testin
Artists: "Different Artist",
DurationMS: 180000,
ISRC: "USRC17607839",
ProviderID: "deezer",
ProviderID: "metadata-b",
},
}
@@ -634,7 +634,7 @@ func TestSelectBestReEnrichTrackPlaceholderFallsBackToAlbum(t *testing.T) {
Artists: "Harry Styles",
AlbumName: "Harry Styles",
DurationMS: 180000,
ProviderID: "deezer",
ProviderID: "metadata-b",
},
}
@@ -12,8 +12,8 @@ func qualityTestManifest(name string, options ...QualityOption) *ExtensionManife
}
func TestExtensionQualityKeepsAudioKindAcrossProviders(t *testing.T) {
amazon := qualityTestManifest("source", QualityOption{ID: "best", Label: "FLAC Best Available"}, QualityOption{ID: "ac4", Label: "Dolby Atmos"})
tidal := qualityTestManifest("target",
source := qualityTestManifest("source", QualityOption{ID: "best", Label: "FLAC Best Available"}, QualityOption{ID: "ac4", Label: "Dolby Atmos"})
target := qualityTestManifest("target",
QualityOption{ID: "DOLBY_ATMOS", Label: "Dolby Atmos", Description: "falls back to FLAC"},
QualityOption{ID: "HI_RES_LOSSLESS", Label: "HiRes FLAC"},
QualityOption{ID: "LOSSLESS", Label: "Lossless"},
@@ -25,13 +25,13 @@ func TestExtensionQualityKeepsAudioKindAcrossProviders(t *testing.T) {
{"ac4", "DOLBY_ATMOS"}, {"DOLBY_ATMOS", "DOLBY_ATMOS"}, {"HIGH", "HIGH"},
} {
t.Run(tc.requested, func(t *testing.T) {
got, err := resolveExtensionDownloadQuality(tc.requested, amazon, tidal)
got, err := resolveExtensionDownloadQuality(tc.requested, source, target)
if err != nil || got != tc.want {
t.Fatalf("quality=%q err=%v; want %q", got, err, tc.want)
}
})
}
if got, err := resolveExtensionDownloadQuality("best", amazon, amazon); err != nil || got != "best" {
if got, err := resolveExtensionDownloadQuality("best", source, source); err != nil || got != "best" {
t.Fatalf("same provider selection changed: %s %v", got, err)
}
}
@@ -8,7 +8,7 @@ func TestOverlayExtensionReleaseMetadataFillsMissingRequestFields(t *testing.T)
AlbumType: "album",
Explicit: true,
UPC: "4006381333931",
Comment: "https://music.apple.com/jp/album/1532211596",
Comment: "https://source.example/album/1532211596",
}
overlayExtensionReleaseMetadata(&req, track)
@@ -29,7 +29,7 @@ func TestOverlayExtensionReleaseMetadataFillsMissingRequestFields(t *testing.T)
response := buildDownloadSuccessResponse(
req,
DownloadResult{},
"amazon",
"download-provider",
"downloaded",
"song.m4a",
false,
+2 -2
View File
@@ -36,7 +36,7 @@ func TestVerifiedDownloadResumeTriesSelectedProviderBeforeMetadata(t *testing.T)
ItemID: "resume-item",
Service: downloadExt.ID,
Source: metadataExt.ID,
SpotifyID: "spotify:track:1",
SpotifyID: "source-track-1",
TrackName: "Original Song",
ArtistName: "Artist",
AlbumName: "Album",
@@ -93,7 +93,7 @@ func TestVerifiedDownloadResumeReusesPreparedMetadata(t *testing.T) {
ItemID: "prepared-item",
Service: downloadExt.ID,
Source: metadataExt.ID,
SpotifyID: "spotify:track:2",
SpotifyID: "source-track-2",
TrackName: "Original Song",
ArtistName: "Artist",
AlbumName: "Album",
@@ -16,7 +16,7 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) {
if status, _ := classifyExtensionHealthBody([]byte(`not-json`), ""); status != "online" {
t.Fatalf("invalid JSON status = %q", status)
}
if status, msg := classifyExtensionHealthBody([]byte(`{"services":{"tidal":{"status":401,"label":"Tidal","detail":"auth_required"}}}`), "tidal"); status != "degraded" || !strings.Contains(msg, "Tidal") {
if status, msg := classifyExtensionHealthBody([]byte(`{"services":{"provider":{"status":401,"label":"Provider","detail":"auth_required"}}}`), "provider"); status != "degraded" || !strings.Contains(msg, "Provider") {
t.Fatalf("service status/message = %q/%q", status, msg)
}
if status, msg, ok := classifyExtensionHealthService(map[string]any{"services": map[string]any{}}, "missing"); !ok || status != "unknown" || !strings.Contains(msg, "missing") {
@@ -68,7 +68,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
if err != nil {
t.Fatalf("SearchTracks: %v", err)
}
if search.Total != 1 || search.Tracks[0].ProviderID != ext.ID || search.Tracks[0].ExternalLinks["tidal"] == "" {
if search.Total != 1 || search.Tracks[0].ProviderID != ext.ID || search.Tracks[0].ExternalLinks["provider"] == "" {
t.Fatalf("search = %#v", search)
}
@@ -112,7 +112,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
t.Fatalf("enriched = %#v", enriched)
}
availability, err := provider.CheckAvailabilityForItemID("ISRC", "Song", "Artist", "spotify:1", "dz", "tidal", "qobuz", 0, "")
availability, err := provider.CheckAvailabilityForItemID("ISRC", "Song", "Artist", "source-id", "alternate-id-a", "alternate-id-b", "alternate-id-c", 0, "")
if err != nil {
t.Fatalf("CheckAvailabilityForItemID: %v", err)
}
@@ -172,15 +172,15 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) {
func TestExtensionProviderAndManagerSelectionHelpers(t *testing.T) {
manifest := &ExtensionManifest{Capabilities: map[string]any{
"replacesBuiltInProviders": []any{" Deezer ", 7, ""},
"replacesBuiltInProviders": []any{" Legacy-Provider ", 7, ""},
}}
if values := manifestCapabilityStringList(manifest, "replacesBuiltInProviders"); len(values) != 1 || values[0] != "deezer" {
if values := manifestCapabilityStringList(manifest, "replacesBuiltInProviders"); len(values) != 1 || values[0] != "legacy-provider" {
t.Fatalf("capability list = %#v", values)
}
if !extensionReplacesBuiltInProvider(&loadedExtension{Manifest: manifest}, "deezer") || extensionReplacesBuiltInProvider(nil, "deezer") {
if !extensionReplacesBuiltInProvider(&loadedExtension{Manifest: manifest}, "legacy-provider") || extensionReplacesBuiltInProvider(nil, "legacy-provider") {
t.Fatal("extension replacement mismatch")
}
if trimKnownProviderPrefix("Deezer:101", "deezer") != "101" || trimKnownProviderPrefix("101", "deezer") != "101" {
if trimKnownProviderPrefix("Legacy-Provider:101", "legacy-provider") != "101" || trimKnownProviderPrefix("101", "legacy-provider") != "101" {
t.Fatal("trimKnownProviderPrefix mismatch")
}
if metadataTrackDedupKey(ExtTrackMetadata{ISRC: "usrc"}) != "isrc:USRC" ||
+51 -51
View File
@@ -133,10 +133,10 @@ func TestSetProviderPriorityKeepsExtensionNamedLikeRetiredDownloader(t *testing.
func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *testing.T) {
manager := getExtensionManager()
amazon := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
amazon.ID = "amazon"
amazon.Manifest.Name = "amazon"
amazon.Manifest.ServiceHealth = []ExtensionHealthCheck{{
unavailable := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
unavailable.ID = "unavailable-provider"
unavailable.Manifest.Name = "unavailable-provider"
unavailable.Manifest.ServiceHealth = []ExtensionHealthCheck{{
ID: "main",
URL: "://bad",
Required: true,
@@ -146,59 +146,59 @@ func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *test
plain.ID = "plain"
plain.Manifest.Name = "plain"
deezer := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
deezer.ID = "deezer"
deezer.Manifest.Name = "deezer"
deezer.Manifest.ServiceHealth = []ExtensionHealthCheck{{
available := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
available.ID = "available-provider"
available.Manifest.Name = "available-provider"
available.Manifest.ServiceHealth = []ExtensionHealthCheck{{
ID: "main",
URL: "https://example.test/health",
}}
manager.mu.Lock()
previousAmazon, hadAmazon := manager.extensions[amazon.ID]
previousUnavailable, hadUnavailable := manager.extensions[unavailable.ID]
previousPlain, hadPlain := manager.extensions[plain.ID]
previousDeezer, hadDeezer := manager.extensions[deezer.ID]
manager.extensions[amazon.ID] = amazon
previousAvailable, hadAvailable := manager.extensions[available.ID]
manager.extensions[unavailable.ID] = unavailable
manager.extensions[plain.ID] = plain
manager.extensions[deezer.ID] = deezer
manager.extensions[available.ID] = available
manager.mu.Unlock()
defer func() {
manager.mu.Lock()
if hadAmazon {
manager.extensions[amazon.ID] = previousAmazon
if hadUnavailable {
manager.extensions[unavailable.ID] = previousUnavailable
} else {
delete(manager.extensions, amazon.ID)
delete(manager.extensions, unavailable.ID)
}
if hadPlain {
manager.extensions[plain.ID] = previousPlain
} else {
delete(manager.extensions, plain.ID)
}
if hadDeezer {
manager.extensions[deezer.ID] = previousDeezer
if hadAvailable {
manager.extensions[available.ID] = previousAvailable
} else {
delete(manager.extensions, deezer.ID)
delete(manager.extensions, available.ID)
}
manager.mu.Unlock()
extensionHealthCacheMu.Lock()
delete(extensionHealthCache, amazon.ID)
delete(extensionHealthCache, deezer.ID)
delete(extensionHealthCache, unavailable.ID)
delete(extensionHealthCache, available.ID)
extensionHealthCacheMu.Unlock()
}()
extensionHealthCacheMu.Lock()
extensionHealthCache[amazon.ID] = cachedExtensionHealthResult{
extensionHealthCache[unavailable.ID] = cachedExtensionHealthResult{
result: ExtensionHealthResult{
ExtensionID: amazon.ID,
ExtensionID: unavailable.ID,
Status: "offline",
CheckedAt: time.Now().UTC().Format(time.RFC3339),
},
expiresAt: time.Now().Add(time.Minute),
}
extensionHealthCache[deezer.ID] = cachedExtensionHealthResult{
extensionHealthCache[available.ID] = cachedExtensionHealthResult{
result: ExtensionHealthResult{
ExtensionID: deezer.ID,
ExtensionID: available.ID,
Status: "online",
CheckedAt: time.Now().UTC().Format(time.RFC3339),
},
@@ -207,11 +207,11 @@ func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *test
extensionHealthCacheMu.Unlock()
got := prioritizeFallbackProvidersByHealth(
[]string{"amazon", "plain", "deezer"},
[]string{"unavailable-provider", "plain", "available-provider"},
manager,
"",
)
want := []string{"deezer", "plain"}
want := []string{"available-provider", "plain"}
if len(got) != len(want) {
t.Fatalf("unexpected provider order length: got %v want %v", got, want)
}
@@ -468,14 +468,14 @@ func TestShouldStopProviderFallback(t *testing.T) {
}
func TestMoveProviderToFrontPreservesExplicitSelection(t *testing.T) {
priority := []string{"qobuz-web", "amazon-web", "tidal-web"}
got := moveProviderToFront(priority, "AMAZON-WEB")
want := []string{"amazon-web", "qobuz-web", "tidal-web"}
priority := []string{"provider-a", "provider-b", "provider-c"}
got := moveProviderToFront(priority, "PROVIDER-B")
want := []string{"provider-b", "provider-a", "provider-c"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("moveProviderToFront() = %#v, want %#v", got, want)
}
if !reflect.DeepEqual(priority, []string{"qobuz-web", "amazon-web", "tidal-web"}) {
if !reflect.DeepEqual(priority, []string{"provider-a", "provider-b", "provider-c"}) {
t.Fatalf("moveProviderToFront mutated input: %#v", priority)
}
}
@@ -520,15 +520,15 @@ func TestDiscardRejectedExtensionOutputPreservesExistingLibraryHit(t *testing.T)
}
func TestBuildExtensionFallbackStoppedResponsePrefersAvailabilityReason(t *testing.T) {
resp := buildExtensionFallbackStoppedResponse("soundcloud", &ExtAvailabilityResult{
Reason: "direct SoundCloud track ID",
resp := buildExtensionFallbackStoppedResponse("direct-provider", &ExtAvailabilityResult{
Reason: "direct provider track ID",
SkipFallback: true,
}, errors.New("ignored"))
if resp.Service != "soundcloud" {
if resp.Service != "direct-provider" {
t.Fatalf("service = %q", resp.Service)
}
if resp.Error != "Fallback stopped by soundcloud: direct SoundCloud track ID" {
if resp.Error != "Fallback stopped by direct-provider: direct provider track ID" {
t.Fatalf("unexpected error message: %q", resp.Error)
}
if resp.ErrorType != "extension_error" {
@@ -537,11 +537,11 @@ func TestBuildExtensionFallbackStoppedResponsePrefersAvailabilityReason(t *testi
}
func TestBuildExtensionFallbackStoppedResponseFallsBackToError(t *testing.T) {
resp := buildExtensionFallbackStoppedResponse("soundcloud", &ExtAvailabilityResult{
resp := buildExtensionFallbackStoppedResponse("direct-provider", &ExtAvailabilityResult{
SkipFallback: true,
}, errors.New("lookup failed"))
if resp.Error != "Fallback stopped by soundcloud: lookup failed" {
if resp.Error != "Fallback stopped by direct-provider: lookup failed" {
t.Fatalf("unexpected error message: %q", resp.Error)
}
}
@@ -619,7 +619,7 @@ func TestParseExtensionSearchResultAcceptsObjectAndArrayShapes(t *testing.T) {
album_name: "Album",
duration_ms: 123000,
cover_url: "https://img.test/cover.jpg",
external_links: { spotify: "spotify:track:1" },
external_links: { provider: "https://provider.example/track/1" },
audio_quality: "LOSSLESS"
}],
total: 9
@@ -640,7 +640,7 @@ func TestParseExtensionSearchResultAcceptsObjectAndArrayShapes(t *testing.T) {
track.AlbumName != "Album" ||
track.DurationMS != 123000 ||
track.CoverURL != "https://img.test/cover.jpg" ||
track.ExternalLinks["spotify"] != "spotify:track:1" ||
track.ExternalLinks["provider"] != "https://provider.example/track/1" ||
track.AudioQuality != "LOSSLESS" {
t.Fatalf("unexpected parsed track: %+v", track)
}
@@ -846,16 +846,16 @@ func TestMatchesURLHostAnchored(t *testing.T) {
manifest := &ExtensionManifest{
URLHandler: &URLHandlerConfig{
Enabled: true,
Patterns: []string{"spotify.com", "deezer.page.link", "spotify:"},
Patterns: []string{"catalog.example", "short.example", "catalog:"},
},
}
for _, urlStr := range []string{
"https://open.spotify.com/track/abc",
"https://spotify.com/track/abc",
"HTTPS://OPEN.SPOTIFY.COM/track/ABC",
"https://deezer.page.link/xyz",
"spotify:track:abc123",
"https://open.catalog.example/track/abc",
"https://catalog.example/track/abc",
"HTTPS://OPEN.CATALOG.EXAMPLE/track/ABC",
"https://short.example/xyz",
"catalog:track:abc123",
} {
if !manifest.MatchesURL(urlStr) {
t.Fatalf("expected match for %q", urlStr)
@@ -864,10 +864,10 @@ func TestMatchesURLHostAnchored(t *testing.T) {
for _, urlStr := range []string{
// The old substring matching accepted all of these.
"https://evil.example/?next=https://spotify.com/track/abc",
"https://notspotify.com/track/abc",
"https://spotify.com.evil.example/track/abc",
"https://example.com/spotify.com",
"https://evil.example/?next=https://catalog.example/track/abc",
"https://notcatalog.example/track/abc",
"https://catalog.example.evil.example/track/abc",
"https://example.com/catalog.example",
"not a url at all",
} {
if manifest.MatchesURL(urlStr) {
@@ -878,13 +878,13 @@ func TestMatchesURLHostAnchored(t *testing.T) {
withPath := &ExtensionManifest{
URLHandler: &URLHandlerConfig{
Enabled: true,
Patterns: []string{"youtube.com/watch"},
Patterns: []string{"video.example/watch"},
},
}
if !withPath.MatchesURL("https://www.youtube.com/watch?v=abc") {
if !withPath.MatchesURL("https://www.video.example/watch?v=abc") {
t.Fatal("expected host+path prefix to match")
}
if withPath.MatchesURL("https://www.youtube.com/playlist?list=abc") {
if withPath.MatchesURL("https://www.video.example/playlist?list=abc") {
t.Fatal("expected different path to not match")
}
}
@@ -1170,7 +1170,7 @@ func TestExtensionRuntimeUtilityAPIs(t *testing.T) {
}
func TestClassifySignedSessionExpiredAsVerification(t *testing.T) {
got := classifyDownloadErrorType("Failed to resolve Deezer download: signed session expired")
got := classifyDownloadErrorType("Failed to resolve provider download: signed session expired")
if got != "verification_required" {
t.Fatalf("expected verification_required, got %q", got)
}
+40 -40
View File
@@ -81,7 +81,7 @@ func TestSignedSessionConfigWithDefaults(t *testing.T) {
t.Run("preserves values the manifest already set", func(t *testing.T) {
custom := &SignedSessionConfig{
Namespace: "tidal",
Namespace: "provider",
BaseURL: "https://auth.example.com",
AppVersion: "5.0",
Platform: "mobile",
@@ -89,7 +89,7 @@ func TestSignedSessionConfigWithDefaults(t *testing.T) {
Endpoints: SignedSessionEndpoints{Exchange: "/custom/exchange"},
}
got := signedSessionConfigWithDefaults(custom)
if got.Namespace != "tidal" || got.BaseURL != "https://auth.example.com" {
if got.Namespace != "provider" || got.BaseURL != "https://auth.example.com" {
t.Errorf("namespace/baseUrl were overwritten: %+v", got)
}
if got.AppVersion != "5.0" || got.Platform != "mobile" || got.TimeWindowSeconds != 60 {
@@ -360,7 +360,7 @@ func TestDownloadWithExtensionsPreflightsBeforeMetadataEnrichment(t *testing.T)
// Metadata may originate from another extension, but the explicitly chosen
// download provider owns the signed-session namespace and verification.
requestJSON := `{"source":"spotify-metadata","service":"preflight-download","item_id":"preflight-item","isrc":"USRC17607839"}`
requestJSON := `{"source":"source-metadata","service":"preflight-download","item_id":"preflight-item","isrc":"USRC17607839"}`
responseJSON, err := DownloadWithExtensionsJSON(requestJSON)
if err != nil {
t.Fatalf("DownloadWithExtensionsJSON: %v", err)
@@ -718,12 +718,12 @@ func TestSignedSessionProviderRetryDuration(t *testing.T) {
}
func TestNormalizeSignedSessionRecordScope(t *testing.T) {
config := SignedSessionConfig{Namespace: "Tidal", BaseURL: "https://a.example.com", AppVersion: "1.0", Platform: "mobile"}
config := SignedSessionConfig{Namespace: "Provider", BaseURL: "https://a.example.com", AppVersion: "1.0", Platform: "mobile"}
t.Run("first save just stamps the scope", func(t *testing.T) {
record := &signedSessionRecord{SessionID: "s1", SessionSecret: "secret"}
normalizeSignedSessionRecordScope(config, record)
if record.Namespace != "tidal" || record.BaseURL != config.BaseURL {
if record.Namespace != "provider" || record.BaseURL != config.BaseURL {
t.Errorf("scope not stamped: %+v", record)
}
if record.SessionID != "s1" || record.SessionSecret != "secret" {
@@ -733,7 +733,7 @@ func TestNormalizeSignedSessionRecordScope(t *testing.T) {
t.Run("same scope preserves the session", func(t *testing.T) {
record := &signedSessionRecord{
Namespace: "tidal", BaseURL: config.BaseURL, AppVersion: config.AppVersion, Platform: config.Platform,
Namespace: "provider", BaseURL: config.BaseURL, AppVersion: config.AppVersion, Platform: config.Platform,
SessionID: "s1", SessionSecret: "secret", ExpiresAt: "later",
}
normalizeSignedSessionRecordScope(config, record)
@@ -744,7 +744,7 @@ func TestNormalizeSignedSessionRecordScope(t *testing.T) {
t.Run("changed scope wipes the session secret", func(t *testing.T) {
record := &signedSessionRecord{
Namespace: "tidal", BaseURL: "https://old.example.com", AppVersion: config.AppVersion, Platform: config.Platform,
Namespace: "provider", BaseURL: "https://old.example.com", AppVersion: config.AppVersion, Platform: config.Platform,
SessionID: "s1", SessionSecret: "secret", ExpiresAt: "later",
}
normalizeSignedSessionRecordScope(config, record)
@@ -791,10 +791,10 @@ func saveUsableSignedSession(
}
func TestSignedSessionFilePathDeterminism(t *testing.T) {
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
configA := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://a.example.com"}
configB := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://b.example.com"}
configA := SignedSessionConfig{Namespace: "provider", BaseURL: "https://a.example.com"}
configB := SignedSessionConfig{Namespace: "provider", BaseURL: "https://b.example.com"}
pathA1, err := runtime.signedSessionFilePath(configA)
if err != nil {
@@ -822,8 +822,8 @@ func TestSignedSessionFilePathDeterminism(t *testing.T) {
}
func TestLoadAndSaveSignedSessionRoundTrip(t *testing.T) {
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
config := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
config := SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
record, err := runtime.loadSignedSession(config)
if err != nil {
@@ -868,8 +868,8 @@ func TestLoadAndSaveSignedSessionRoundTrip(t *testing.T) {
}
func TestSignedSessionStatusAndClear(t *testing.T) {
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
readStatus := func() map[string]any {
v := runtime.signedSessionStatus(goja.FunctionCall{})
@@ -929,7 +929,7 @@ func TestDoSignedSessionRequestSignature(t *testing.T) {
var capturedErr string
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"})
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"})
prefix := config.HeaderPrefix
ts := req.Header.Get(prefix + "Timestamp")
@@ -982,8 +982,8 @@ func TestDoSignedSessionRequestSignature(t *testing.T) {
}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"})
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
config := signedSessionConfigWithDefaults(&SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"})
record := &signedSessionRecord{InstallID: "install-1", SessionID: sessionID, SessionSecret: sessionSecret}
resp, body, _, err := runtime.doSignedSessionRequest(config, record, http.MethodPost, "/tracks/search", []byte(`{"q":"test"}`), nil)
@@ -1017,8 +1017,8 @@ func TestSignedSessionFetchUnauthenticatedTriggersVerification(t *testing.T) {
return nil, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
call := goja.FunctionCall{Arguments: []goja.Value{runtime.vm.ToValue("GET"), runtime.vm.ToValue("/tracks/search")}}
result := runtime.signedSessionFetch(call).Export().(map[string]any)
@@ -1056,8 +1056,8 @@ func TestSignedSessionFetchRevokesSessionOnCanonicalSessionInvalid(t *testing.T)
}
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
config := SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
config := SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
runtime.manifest.SignedSession = &config
resolved := signedSessionConfigWithDefaults(&config)
@@ -1849,8 +1849,8 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
body, _ := json.Marshal(payload)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
if err := runtime.exchangeSignedSessionGrant("grant-token"); err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -1876,8 +1876,8 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 400, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
if err := runtime.exchangeSignedSessionGrant("bad-grant"); err == nil {
t.Fatal("expected an error for a non-2xx exchange response")
@@ -1919,9 +1919,9 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
Request: req,
}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-rate-limit", transport)
runtime := newSignedSessionTestRuntime(t, "provider-rate-limit", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{
Namespace: "tidal-rate-limit",
Namespace: "provider-rate-limit",
BaseURL: "https://auth.example.com",
}
setPendingSignedSessionGrant(runtime.extensionID, "grant-preserved")
@@ -1964,9 +1964,9 @@ func TestExchangeSignedSessionGrant(t *testing.T) {
Request: req,
}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-rate-limit-exhausted", transport)
runtime := newSignedSessionTestRuntime(t, "provider-rate-limit-exhausted", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{
Namespace: "tidal-rate-limit-exhausted",
Namespace: "provider-rate-limit-exhausted",
BaseURL: "https://auth.example.com",
}
setPendingSignedSessionGrant(runtime.extensionID, "grant-retry-later")
@@ -2025,9 +2025,9 @@ func TestRefreshSignedSession(t *testing.T) {
body, _ := json.Marshal(payload)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
Namespace: "tidal", BaseURL: "https://auth.example.com",
Namespace: "provider", BaseURL: "https://auth.example.com",
Endpoints: SignedSessionEndpoints{Refresh: "/session/refresh"},
})
record := &signedSessionRecord{InstallID: "install-1", SessionID: "sess-1", SessionSecret: "old-secret", ExpiresAt: "2030-01-01T00:00:00Z"}
@@ -2055,9 +2055,9 @@ func TestRefreshSignedSession(t *testing.T) {
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 500, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
Namespace: "tidal", BaseURL: "https://auth.example.com",
Namespace: "provider", BaseURL: "https://auth.example.com",
Endpoints: SignedSessionEndpoints{Refresh: "/session/refresh"},
})
record := &signedSessionRecord{InstallID: "install-1", SessionID: "sess-1", SessionSecret: "old-secret"}
@@ -2229,8 +2229,8 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
body, _ := json.Marshal(payload)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
call := goja.FunctionCall{Arguments: []goja.Value{runtime.vm.ToValue("grant-from-arg")}}
result := runtime.signedSessionCompleteGrant(call).Export().(map[string]any)
@@ -2245,8 +2245,8 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
body, _ := json.Marshal(payload)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(body))), Request: req}, nil
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext-pending", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "tidal", BaseURL: "https://auth.example.com"}
runtime := newSignedSessionTestRuntime(t, "provider-ext-pending", transport)
runtime.manifest.SignedSession = &SignedSessionConfig{Namespace: "provider", BaseURL: "https://auth.example.com"}
setPendingSignedSessionGrant(runtime.extensionID, "pending-grant")
result := runtime.signedSessionCompleteGrant(goja.FunctionCall{}).Export().(map[string]any)
@@ -2263,7 +2263,7 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
})
t.Run("no grant available reports failure", func(t *testing.T) {
runtime := newSignedSessionTestRuntime(t, "tidal-ext-none", nil)
runtime := newSignedSessionTestRuntime(t, "provider-ext-none", nil)
result := runtime.signedSessionCompleteGrant(goja.FunctionCall{}).Export().(map[string]any)
if result["success"] != false {
t.Fatalf("expected failure without a grant, got %+v", result)
@@ -2273,11 +2273,11 @@ func TestSignedSessionCompleteGrant(t *testing.T) {
func TestBuildSignedSessionChallengeURL(t *testing.T) {
config := signedSessionConfigWithDefaults(&SignedSessionConfig{
Namespace: "tidal",
Namespace: "provider",
BaseURL: "https://auth.example.com",
CallbackURL: "spotiflac://session-grant",
})
runtime := newSignedSessionTestRuntime(t, "tidal-ext", nil)
runtime := newSignedSessionTestRuntime(t, "provider-ext", nil)
got := runtime.buildSignedSessionChallengeURL(config, "chal-123", "state-123")
+6 -6
View File
@@ -123,13 +123,13 @@ func TestBuildDownloadFilename_ProvidesTraceabilityPlaceholders(t *testing.T) {
TrackName: "Song Name",
ArtistName: "Artist Name",
ISRC: "USABC1234567",
DownloadProvider: "tidal-web",
DownloadProvider: "download-provider",
ProviderTrackID: "123456789",
FilenameFormat: "{artist} - {title} [{isrc}] [{provider}-{provider_id}]",
OutputExt: ".flac",
})
expected := "Artist Name - Song Name [USABC1234567] [tidal-web-123456789].flac"
expected := "Artist Name - Song Name [USABC1234567] [download-provider-123456789].flac"
if filename != expected {
t.Fatalf("expected %q, got %q", expected, filename)
}
@@ -137,12 +137,12 @@ func TestBuildDownloadFilename_ProvidesTraceabilityPlaceholders(t *testing.T) {
func TestBuildFilenameFromTemplate_TraceabilityAliases(t *testing.T) {
metadata := map[string]any{
"provider": "soundcloud",
"provider": "download-provider",
"provider_id": "998877",
}
formatted := buildFilenameFromTemplate("{platform}-{id}", metadata)
if formatted != "soundcloud-998877" {
if formatted != "download-provider-998877" {
t.Fatalf("unexpected alias filename: %q", formatted)
}
}
@@ -150,14 +150,14 @@ func TestBuildFilenameFromTemplate_TraceabilityAliases(t *testing.T) {
func TestBuildFilenameFromTemplate_CleansEmptyTraceabilityDecorations(t *testing.T) {
metadata := map[string]any{
"title": "Song Name",
"provider": "tidal-web",
"provider": "download-provider",
}
formatted := buildFilenameFromTemplate(
"{title} [{isrc}] [{provider}-{provider_id}]",
metadata,
)
if formatted != "Song Name [tidal-web]" {
if formatted != "Song Name [download-provider]" {
t.Fatalf("unexpected empty placeholder cleanup: %q", formatted)
}
}
+3 -3
View File
@@ -79,8 +79,8 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) {
if ua := appUserAgent(); !strings.Contains(ua, "4.5.0") {
t.Fatalf("user agent = %q", ua)
}
SetLyricsProviderOrder([]string{"LRCLIB", "bad", "extension:Apple-Music", "netease", "extension:apple-music"})
if providers := GetLyricsProviderOrder(); len(providers) != 3 || providers[0] != LyricsProviderLRCLIB || providers[1] != "extension:apple-music" {
SetLyricsProviderOrder([]string{"LRCLIB", "bad", "extension:Lyrics-Fixture", "netease", "extension:lyrics-fixture"})
if providers := GetLyricsProviderOrder(); len(providers) != 3 || providers[0] != LyricsProviderLRCLIB || providers[1] != "extension:lyrics-fixture" {
t.Fatalf("providers = %#v", providers)
}
SetLyricsProviderOrder(nil)
@@ -332,7 +332,7 @@ func TestConcurrentLyricsProvidersReturnFastFallback(t *testing.T) {
func TestResolveLyricsProviderOrderOnlyIncludesSelectedAvailableExtensions(t *testing.T) {
availableExtensions := map[string]*extensionProviderWrapper{
"extension:apple-music": nil,
"extension:lyrics-fixture": nil,
"extension:future-provider": nil,
}
providers := resolveLyricsProviderOrder(
+1 -1
View File
@@ -167,7 +167,7 @@ func TestTrackMatchesRequestRejectsDurationMismatchAcrossReleases(t *testing.T)
func TestTitlesMatch_SeparatorVariants(t *testing.T) {
if !titlesMatch("Doctor / Cops", "Doctor _ Cops") {
t.Fatal("expected tidal titlesMatch to accept / vs _ variant")
t.Fatal("expected titlesMatch to accept / vs _ variant")
}
}
+2 -2
View File
@@ -47,14 +47,14 @@ class RunnerTests: XCTestCase {
func testParsesOAuthCallback() {
let route = ExtensionCallbackParser.parse(
URL(string: "spotiflac://callback?code=auth-code&state=spotify-web")!
URL(string: "spotiflac://callback?code=auth-code&state=metadata-provider")!
)
XCTAssertEqual(
route,
ExtensionCallbackRoute(
code: "auth-code",
state: "spotify-web",
state: "metadata-provider",
isSessionGrant: false
)
)
+2 -2
View File
@@ -15,7 +15,7 @@ DownloadHistoryItem _historyItem({
filePath: filePath,
service: 'test',
downloadedAt: downloadedAt,
spotifyId: 'spotify:track:same',
spotifyId: 'source-track-same',
isrc: 'SAMEISRC',
);
}
@@ -84,7 +84,7 @@ void main() {
totalCount: 2,
);
expect(state.getBySpotifyId('spotify:track:same')?.id, 'newest');
expect(state.getBySpotifyId('source-track-same')?.id, 'newest');
expect(state.getByIsrc('SAMEISRC')?.id, 'newest');
expect(
state.findByTrackAndArtist('Same Song', 'Same Artist')?.id,
@@ -8,43 +8,43 @@ void main() {
test('failed challenge attempts do not consume the granted retry', () {
final guard = DownloadVerificationRetryGuard();
guard.recordVerificationResult('item-1', 'tidal-web', granted: false);
guard.recordVerificationResult('item-1', 'provider-a', granted: false);
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isFalse);
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isFalse);
});
test('completed grants allow only one automatic retry per service', () {
final guard = DownloadVerificationRetryGuard();
guard.recordVerificationResult('item-1', ' TIDAL-WEB ', granted: true);
guard.recordVerificationResult('item-1', ' PROVIDER-A ', granted: true);
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isTrue);
expect(guard.hasRetriedAfterGrant('item-1', 'qobuz-web'), isFalse);
expect(guard.hasRetriedAfterGrant('item-2', 'tidal-web'), isFalse);
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isTrue);
expect(guard.hasRetriedAfterGrant('item-1', 'provider-b'), isFalse);
expect(guard.hasRetriedAfterGrant('item-2', 'provider-a'), isFalse);
});
test('manual retry clears every service marker for an item', () {
final guard = DownloadVerificationRetryGuard()
..recordVerificationResult('item-1', 'tidal-web', granted: true)
..recordVerificationResult('item-1', 'qobuz-web', granted: true)
..recordVerificationResult('item-2', 'tidal-web', granted: true);
..recordVerificationResult('item-1', 'provider-a', granted: true)
..recordVerificationResult('item-1', 'provider-b', granted: true)
..recordVerificationResult('item-2', 'provider-a', granted: true);
guard.clearItem('item-1');
expect(guard.hasRetriedAfterGrant('item-1', 'tidal-web'), isFalse);
expect(guard.hasRetriedAfterGrant('item-1', 'qobuz-web'), isFalse);
expect(guard.hasRetriedAfterGrant('item-2', 'tidal-web'), isTrue);
expect(guard.hasRetriedAfterGrant('item-1', 'provider-a'), isFalse);
expect(guard.hasRetriedAfterGrant('item-1', 'provider-b'), isFalse);
expect(guard.hasRetriedAfterGrant('item-2', 'provider-a'), isTrue);
});
test('queue cleanup retains markers only for remaining items', () {
final guard = DownloadVerificationRetryGuard()
..recordVerificationResult('removed', 'tidal-web', granted: true)
..recordVerificationResult('remaining', 'tidal-web', granted: true);
..recordVerificationResult('removed', 'provider-a', granted: true)
..recordVerificationResult('remaining', 'provider-a', granted: true);
guard.retainItems({'remaining'});
expect(guard.hasRetriedAfterGrant('removed', 'tidal-web'), isFalse);
expect(guard.hasRetriedAfterGrant('remaining', 'tidal-web'), isTrue);
expect(guard.hasRetriedAfterGrant('removed', 'provider-a'), isFalse);
expect(guard.hasRetriedAfterGrant('remaining', 'provider-a'), isTrue);
});
});
@@ -58,7 +58,7 @@ void main() {
final result = coordinator.waitForGrant(
itemId: 'item-1',
service: 'tidal-web',
service: 'provider-a',
startFlow: (cancellationSignal) async {
flowStarted.complete();
await cancellationSignal;
@@ -81,7 +81,7 @@ void main() {
final first = coordinator.waitForGrant(
itemId: 'item-1',
service: 'tidal-web',
service: 'provider-a',
startFlow: (cancellationSignal) async {
starts++;
await cancellationSignal;
@@ -93,7 +93,7 @@ void main() {
final second = coordinator.waitForGrant(
itemId: 'item-2',
service: 'tidal-web',
service: 'provider-a',
startFlow: (_) async {
starts++;
return true;
@@ -122,12 +122,12 @@ void main() {
final first = coordinator.waitForGrant(
itemId: 'item-1',
service: ' TIDAL-WEB ',
service: ' PROVIDER-A ',
startFlow: startFlow,
);
final second = coordinator.waitForGrant(
itemId: 'item-2',
service: 'tidal-web',
service: 'provider-a',
startFlow: startFlow,
);
+24 -24
View File
@@ -680,7 +680,7 @@ void main() {
test('round-trips json with service availability', () {
final track = Track.fromJson({
'id': 'spotify:track:1',
'id': 'source-track-1',
'name': 'Song',
'artistName': 'Artist',
'albumName': 'Album',
@@ -691,7 +691,7 @@ void main() {
expect(track.availability?.tidal, isTrue);
expect(track.availability?.qobuz, isFalse);
expect(track.availability?.deezerId, '31337');
expect(track.toJson()['id'], 'spotify:track:1');
expect(track.toJson()['id'], 'source-track-1');
expect(track.availability!.toJson()['deezer'], isTrue);
});
});
@@ -710,7 +710,7 @@ void main() {
final item = DownloadItem(
id: 'download-1',
track: sampleTrack(),
service: 'tidal',
service: 'provider-a',
createdAt: createdAt,
);
@@ -745,7 +745,7 @@ void main() {
final base = DownloadItem(
id: 'download-1',
track: sampleTrack(),
service: 'qobuz',
service: 'provider-b',
createdAt: DateTime.utc(2026),
filePath: '/music/stale.flac',
error: 'raw backend failure',
@@ -790,7 +790,7 @@ void main() {
'albumName': 'Album',
'duration': 1000,
},
'service': 'deezer',
'service': 'provider-a',
'status': 'failed',
'errorType': 'network',
'createdAt': '2026-05-04T10:00:00.000Z',
@@ -811,7 +811,7 @@ void main() {
final item = DownloadItem(
id: 'download-active',
track: sampleTrack(),
service: 'tidal',
service: 'provider-a',
createdAt: DateTime.utc(2026),
status: DownloadStatus.finalizing,
progress: 0.97,
@@ -937,7 +937,7 @@ void main() {
);
final updated = settings.copyWith(
defaultService: 'tidal',
defaultService: 'provider-a',
embedReplayGain: true,
embeddedCoverMaxDimension: 1000,
lyricsProviders: ['apple_music'],
@@ -950,7 +950,7 @@ void main() {
clearHomeFeedProvider: true,
);
expect(updated.defaultService, 'tidal');
expect(updated.defaultService, 'provider-a');
expect(updated.embedReplayGain, isTrue);
expect(updated.embeddedCoverMaxDimension, 1000);
expect(updated.lyricsProviders, ['apple_music']);
@@ -969,7 +969,7 @@ void main() {
test('round-trips json including recently added settings', () {
const settings = AppSettings(
defaultService: 'qobuz',
defaultService: 'provider-b',
storageMode: 'saf',
downloadTreeUri: 'content://tree/music',
downloadFallbackExtensionIds: ['ext.a', 'ext.b'],
@@ -996,7 +996,7 @@ void main() {
final decoded = AppSettings.fromJson(settings.toJson());
expect(decoded.defaultService, 'qobuz');
expect(decoded.defaultService, 'provider-b');
expect(decoded.storageMode, 'saf');
expect(decoded.downloadTreeUri, 'content://tree/music');
expect(decoded.downloadFallbackExtensionIds, ['ext.a', 'ext.b']);
@@ -1053,10 +1053,10 @@ void main() {
test('serializes all backend field names', () {
const payload = DownloadRequestPayload(
isrc: 'ISRC123',
service: 'tidal',
downloadProvider: 'tidal-web',
service: 'source-provider',
downloadProvider: 'download-provider',
providerTrackId: '123456789',
spotifyId: 'spotify:track:1',
spotifyId: 'source-track-1',
trackName: 'Song',
artistName: 'Artist',
albumName: 'Album',
@@ -1092,9 +1092,9 @@ void main() {
explicit: true,
albumType: 'compilation',
upc: '0012345678901',
tidalId: 'tidal-1',
qobuzId: 'qobuz-1',
deezerId: 'deezer-1',
tidalId: 'alternate-id-a',
qobuzId: 'alternate-id-b',
deezerId: 'alternate-id-c',
lyricsMode: 'sidecar',
useExtensions: true,
useFallback: true,
@@ -1114,10 +1114,10 @@ void main() {
expect(payload.toJson(), {
'contract_version': DownloadRequestPayload.nativeWorkerContractVersion,
'isrc': 'ISRC123',
'service': 'tidal',
'download_provider': 'tidal-web',
'service': 'source-provider',
'download_provider': 'download-provider',
'provider_track_id': '123456789',
'spotify_id': 'spotify:track:1',
'spotify_id': 'source-track-1',
'track_name': 'Song',
'artist_name': 'Artist',
'album_name': 'Album',
@@ -1153,9 +1153,9 @@ void main() {
'explicit': true,
'album_type': 'compilation',
'upc': '0012345678901',
'tidal_id': 'tidal-1',
'qobuz_id': 'qobuz-1',
'deezer_id': 'deezer-1',
'tidal_id': 'alternate-id-a',
'qobuz_id': 'alternate-id-b',
'deezer_id': 'alternate-id-c',
'lyrics_mode': 'sidecar',
'use_extensions': true,
'use_fallback': true,
@@ -1283,7 +1283,7 @@ void main() {
'albumType': 'album',
'explicit': true,
'upc': '4006381333931',
'comment': 'https://listen.tidal.com/album/1',
'comment': 'https://media.example/album/1',
'format': 'flac',
});
@@ -1302,7 +1302,7 @@ void main() {
expect(normalized['album_type'], 'album');
expect(normalized['explicit'], isTrue);
expect(normalized['upc'], '4006381333931');
expect(normalized['comment'], 'https://listen.tidal.com/album/1');
expect(normalized['comment'], 'https://media.example/album/1');
expect(normalized['audio_codec'], 'flac');
});
+3 -3
View File
@@ -31,11 +31,11 @@ void main() {
addTearDown(subscription.cancel);
final first = PlatformBridge.completeExtensionSessionGrant(
' qobuz-web ',
' provider-a ',
'grant-value',
);
final second = PlatformBridge.completeExtensionSessionGrant(
'QOBUZ-WEB',
'PROVIDER-A',
'grant-value',
);
@@ -48,7 +48,7 @@ void main() {
expect(callCount, 1);
expect(events, hasLength(1));
expect(events.single.extensionId, 'qobuz-web');
expect(events.single.extensionId, 'provider-a');
expect(events.single.success, isTrue);
});
}
+2 -2
View File
@@ -45,10 +45,10 @@ void main() {
test('prefers a known provider id over guessing from the resource id', () {
expect(
resolvePreferredMetadataProviderId(
'apple-music-ext',
'sample-provider',
'37i9dQZF1DXcBWIGoYBM5M',
),
'apple-music-ext',
'sample-provider',
);
});
@@ -20,7 +20,7 @@ void main() {
artistName: 'Artist',
albumName: 'Album',
filePath: r'Z:\missing\track.flac',
service: 'tidal-web',
service: 'provider-a',
downloadedAt: DateTime(2026),
duration: 250,
bitDepth: 16,
@@ -45,7 +45,7 @@ void main() {
expect(headerMeta, findsOneWidget);
expect(find.byType(ExplicitBadge), findsNWidgets(2));
expect(find.text('Explicit'), findsNothing);
for (final label in const ['16-bit/44.1kHz', '4:10', 'Tidal-web']) {
for (final label in const ['16-bit/44.1kHz', '4:10', 'Provider-a']) {
final text = tester.widget<Text>(
find.descendant(of: headerMeta, matching: find.text(label)),
);