navidrome/ui/src/subsonic/index.test.js
Deluan f498dfde7d fix(album): make cover cache keys immune to future file mtimes
All cover cache keys used max(updated_at, cover_art_updated_at), so an album
whose updated_at is in the future (bad file mtimes) would mask a cover edit
entirely — id unchanged, reader cache key unchanged, UI URL unchanged — and
keep serving the old image.

Treat the keys as discriminators instead of dates: the album artwork id adds
the two timestamps, the reader cache keys (album/disc/mediafile) carry the
cover stamp as a separate component, and the UI cache-buster joins both
timestamps instead of picking the newest.
2026-07-17 23:03:20 -04:00

261 lines
6.8 KiB
JavaScript

import { vi } from 'vitest'
import config from '../config'
import subsonic from './index'
describe('getCoverArtUrl', () => {
beforeEach(() => {
// Mock window.location
delete window.location
window.location = { href: 'http://localhost:3000/app' }
// Mock localStorage values required by subsonic
const localStorageMock = {
getItem: vi.fn((key) => {
const values = {
username: 'testuser',
'subsonic-token': 'testtoken',
'subsonic-salt': 'testsalt',
}
return values[key] || null
}),
setItem: vi.fn(),
clear: vi.fn(),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('should return playlist cover art URL for records with sync property', () => {
const playlistRecord = {
id: 'playlist-123',
sync: true,
updatedAt: '2023-01-01T00:00:00Z',
}
const url = subsonic.getCoverArtUrl(
playlistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('pl-playlist-123')
expect(url).toContain('size=600')
expect(url).toContain('square=true')
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
})
it('should add timestamp for playlists without updatedAt', () => {
const playlistRecord = {
id: 'playlist-123',
sync: true,
}
const url = subsonic.getCoverArtUrl(
playlistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('pl-playlist-123')
expect(url).toContain('size=600')
expect(url).toContain('square=true')
expect(url).not.toContain('_=')
})
it('should return album cover art URL for records with albumArtist', () => {
const albumRecord = {
id: 'album-123',
albumArtist: 'Test Artist',
updatedAt: '2023-01-01T00:00:00Z',
}
const url = subsonic.getCoverArtUrl(
albumRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('al-album-123')
expect(url).toContain('size=600')
expect(url).toContain('square=true')
})
it('should include coverArtUpdatedAt in the cache key', () => {
const albumRecord = {
id: 'album-123',
albumArtist: 'Test Artist',
updatedAt: '2023-01-01T00:00:00Z',
coverArtUpdatedAt: '2024-06-01T00:00:00Z',
}
const url = subsonic.getCoverArtUrl(albumRecord)
expect(url).toContain('al-album-123')
expect(url).toContain(
'_=2023-01-01T00%3A00%3A00Z%7C2024-06-01T00%3A00%3A00Z',
)
})
it('should return media file cover art URL for records with album', () => {
const songRecord = {
id: 'song-123',
album: 'Test Album',
updatedAt: '2023-01-01T00:00:00Z',
}
const url = subsonic.getCoverArtUrl(songRecord, config.uiCoverArtSize, true)
expect(url).toContain('mf-song-123')
expect(url).toContain('size=600')
expect(url).toContain('square=true')
})
it('should return artist cover art URL for other records', () => {
const artistRecord = {
id: 'artist-123',
updatedAt: '2023-01-01T00:00:00Z',
}
const url = subsonic.getCoverArtUrl(
artistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('ar-artist-123')
expect(url).toContain('size=600')
expect(url).toContain('square=true')
})
it('should handle records without updatedAt', () => {
const record = {
id: 'test-123',
}
const url = subsonic.getCoverArtUrl(record)
expect(url).toContain('ar-test-123')
expect(url).not.toContain('_=')
})
})
describe('getDiscCoverArtUrl', () => {
beforeEach(() => {
const localStorageMock = {
getItem: vi.fn((key) => {
const values = {
username: 'testuser',
'subsonic-token': 'testtoken',
'subsonic-salt': 'testsalt',
}
return values[key] || null
}),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('should construct URL with dc-albumId:discNumber format, size, and cache param', () => {
const url = subsonic.getDiscCoverArtUrl(
{
albumId: 'album-123',
discNumber: 2,
updatedAt: '2023-01-01T00:00:00Z',
},
48,
)
expect(url).toContain('getCoverArt')
expect(url).toContain('id=dc-album-123%3A2')
expect(url).toContain('size=48')
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
})
it('should handle missing updatedAt', () => {
const url = subsonic.getDiscCoverArtUrl(
{ albumId: 'album-123', discNumber: 1 },
48,
)
expect(url).toContain('id=dc-album-123%3A1')
expect(url).toContain('size=48')
expect(url).not.toContain('_=')
})
it('should handle missing size', () => {
const url = subsonic.getDiscCoverArtUrl({
albumId: 'album-123',
discNumber: 1,
updatedAt: '2023-01-01T00:00:00Z',
})
expect(url).toContain('id=dc-album-123%3A1')
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
expect(url).not.toContain('size=')
})
it('should include coverArtUpdatedAt in the cache key', () => {
const url = subsonic.getDiscCoverArtUrl({
albumId: 'album-123',
discNumber: 1,
updatedAt: '2023-01-01T00:00:00Z',
coverArtUpdatedAt: '2024-06-01T00:00:00Z',
})
expect(url).toContain('id=dc-album-123%3A1')
expect(url).toContain(
'_=2023-01-01T00%3A00%3A00Z%7C2024-06-01T00%3A00%3A00Z',
)
})
})
describe('getAvatarUrl', () => {
beforeEach(() => {
// Mock localStorage values required by subsonic
const localStorageMock = {
getItem: vi.fn((key) => {
const values = {
username: 'testuser',
'subsonic-token': 'testtoken',
'subsonic-salt': 'testsalt',
}
return values[key] || null
}),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('should include username parameter', () => {
const url = subsonic.getAvatarUrl('john')
expect(url).toContain('getAvatar')
expect(url).toContain('username=john')
})
})
describe('reportPlayback', () => {
beforeEach(() => {
const localStorageMock = {
getItem: vi.fn((key) => {
const values = {
username: 'testuser',
'subsonic-token': 'testtoken',
'subsonic-salt': 'testsalt',
}
return values[key] || null
}),
}
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
})
it('should construct reportPlayback URL with correct parameters', () => {
const url = subsonic.url('reportPlayback', null, {
mediaId: 'song-123',
mediaType: 'song',
positionMs: 5000,
state: 'playing',
})
expect(url).toContain('reportPlayback')
expect(url).toContain('mediaId=song-123')
expect(url).toContain('mediaType=song')
expect(url).toContain('positionMs=5000')
expect(url).toContain('state=playing')
})
})