cad9cdc53e
* fix: preserve created_at when moving songs between libraries (#5050) When songs are moved between libraries, their creation date was being reset to the current time, causing them to incorrectly appear in "Recently Added". Three changes fix this: 1. Add hash:"ignore" to AlbumID in MediaFile struct so that Equals() works for cross-library moves (AlbumID includes library prefix, making hashes always differ between libraries) 2. Preserve album created_at in moveMatched() via CopyAttributes, matching the pattern already used in persistAlbum() for within-library album ID changes 3. Only set CreatedAt in Put() when it's zero (new files), and explicitly copy missing.CreatedAt to the target in moveMatched() as defense-in-depth for the INSERT code path * test: add regression tests for created_at preservation (#5050) Add tests covering the three aspects of the fix: - Scanner: moveMatched preserves missing track's created_at - Scanner: CopyAttributes called for album created_at on album change - Scanner: CopyAttributes not called when album ID stays the same - Persistence: Put sets CreatedAt to now for new files with zero value - Persistence: Put preserves non-zero CreatedAt on insert - Persistence: Put does not reset CreatedAt on update Also adds CopyAttributes to MockAlbumRepo for test support. * test: verify album created_at is updated in cross-library move test (#5050) Added end-to-end assertion in the cross-library move test to verify that the new album's CreatedAt field is actually set to the original value after CopyAttributes runs, not just that the method was called. This strengthens the test by confirming the mock correctly propagates the timestamp.
189 lines
4.1 KiB
Go
189 lines
4.1 KiB
Go
package tests
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/id"
|
|
)
|
|
|
|
func CreateMockAlbumRepo() *MockAlbumRepo {
|
|
return &MockAlbumRepo{
|
|
Data: make(map[string]*model.Album),
|
|
}
|
|
}
|
|
|
|
type MockAlbumRepo struct {
|
|
model.AlbumRepository
|
|
Data map[string]*model.Album
|
|
All model.Albums
|
|
Err bool
|
|
Options model.QueryOptions
|
|
ReassignAnnotationCalls map[string]string // prevID -> newID
|
|
CopyAttributesCalls map[string]string // fromID -> toID
|
|
}
|
|
|
|
func (m *MockAlbumRepo) SetError(err bool) {
|
|
m.Err = err
|
|
}
|
|
|
|
func (m *MockAlbumRepo) SetData(albums model.Albums) {
|
|
m.Data = make(map[string]*model.Album, len(albums))
|
|
m.All = albums
|
|
for i, a := range m.All {
|
|
m.Data[a.ID] = &m.All[i]
|
|
}
|
|
}
|
|
|
|
func (m *MockAlbumRepo) Exists(id string) (bool, error) {
|
|
if m.Err {
|
|
return false, errors.New("unexpected error")
|
|
}
|
|
_, found := m.Data[id]
|
|
return found, nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) Get(id string) (*model.Album, error) {
|
|
if m.Err {
|
|
return nil, errors.New("unexpected error")
|
|
}
|
|
if d, ok := m.Data[id]; ok {
|
|
return d, nil
|
|
}
|
|
return nil, model.ErrNotFound
|
|
}
|
|
|
|
func (m *MockAlbumRepo) Put(al *model.Album) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
if al.ID == "" {
|
|
al.ID = id.NewRandom()
|
|
}
|
|
m.Data[al.ID] = al
|
|
return nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) GetAll(qo ...model.QueryOptions) (model.Albums, error) {
|
|
if len(qo) > 0 {
|
|
m.Options = qo[0]
|
|
}
|
|
if m.Err {
|
|
return nil, errors.New("unexpected error")
|
|
}
|
|
return m.All, nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
if d, ok := m.Data[id]; ok {
|
|
d.PlayCount++
|
|
d.PlayDate = ×tamp
|
|
return nil
|
|
}
|
|
return model.ErrNotFound
|
|
}
|
|
func (m *MockAlbumRepo) CountAll(...model.QueryOptions) (int64, error) {
|
|
return int64(len(m.All)), nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) GetTouchedAlbums(libID int) (model.AlbumCursor, error) {
|
|
if m.Err {
|
|
return nil, errors.New("unexpected error")
|
|
}
|
|
return func(yield func(model.Album, error) bool) {
|
|
for _, a := range m.Data {
|
|
if a.ID == "error" {
|
|
if !yield(*a, errors.New("error")) {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
if a.LibraryID != libID {
|
|
continue
|
|
}
|
|
if !yield(*a, nil) {
|
|
break
|
|
}
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) UpdateExternalInfo(album *model.Album) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockAlbumRepo) Search(q string, offset int, size int, options ...model.QueryOptions) (model.Albums, error) {
|
|
if len(options) > 0 {
|
|
m.Options = options[0]
|
|
}
|
|
if m.Err {
|
|
return nil, errors.New("unexpected error")
|
|
}
|
|
// Simple mock implementation - just return all albums for testing
|
|
return m.All, nil
|
|
}
|
|
|
|
// ReassignAnnotation reassigns annotations from one album to another
|
|
func (m *MockAlbumRepo) ReassignAnnotation(prevID string, newID string) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
// Mock implementation - track the reassignment calls
|
|
if m.ReassignAnnotationCalls == nil {
|
|
m.ReassignAnnotationCalls = make(map[string]string)
|
|
}
|
|
m.ReassignAnnotationCalls[prevID] = newID
|
|
return nil
|
|
}
|
|
|
|
// CopyAttributes copies attributes from one album to another
|
|
func (m *MockAlbumRepo) CopyAttributes(fromID, toID string, columns ...string) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
from, ok := m.Data[fromID]
|
|
if !ok {
|
|
return model.ErrNotFound
|
|
}
|
|
to, ok := m.Data[toID]
|
|
if !ok {
|
|
return model.ErrNotFound
|
|
}
|
|
for _, col := range columns {
|
|
switch col {
|
|
case "created_at":
|
|
to.CreatedAt = from.CreatedAt
|
|
}
|
|
}
|
|
if m.CopyAttributesCalls == nil {
|
|
m.CopyAttributesCalls = make(map[string]string)
|
|
}
|
|
m.CopyAttributesCalls[fromID] = toID
|
|
return nil
|
|
}
|
|
|
|
// SetRating sets the rating for an album
|
|
func (m *MockAlbumRepo) SetRating(rating int, itemID string) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetStar sets the starred status for albums
|
|
func (m *MockAlbumRepo) SetStar(starred bool, itemIDs ...string) error {
|
|
if m.Err {
|
|
return errors.New("unexpected error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ model.AlbumRepository = (*MockAlbumRepo)(nil)
|