package tests import ( "context" "maps" "sync" "time" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" ) type MockArtworkRepo struct { model.ArtworkRepository // mu guards the maps so the worker's concurrent drain can hit this mock race-free. mu sync.Mutex Data map[string]model.Artwork ItemData map[string]model.ItemArtwork // keyed by iaKey(kind, id, imageType) Err error // ExistingIDs, keyed by item_kind, backs PurgeDanglingItems; nil map keeps everything. ExistingIDs map[string]map[string]bool } func CreateMockArtworkRepo() *MockArtworkRepo { return &MockArtworkRepo{Data: map[string]model.Artwork{}, ItemData: map[string]model.ItemArtwork{}} } func iaKey(kind, id, imageType string) string { return kind + "|" + id + "|" + imageType } func (m *MockArtworkRepo) GetImage(_ context.Context, hash string) (*model.Artwork, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return nil, m.Err } if a, ok := m.Data[hash]; ok { return &a, nil } return nil, model.ErrNotFound } func (m *MockArtworkRepo) PutImage(_ context.Context, a *model.Artwork) error { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return m.Err } // Mirrors the SQL repository: every upsert refreshes created_at. Age fixtures via Data directly. a.CreatedAt = time.Now() m.Data[a.Hash] = *a return nil } func (m *MockArtworkRepo) GetMimeByHash(context.Context) (map[string]string, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return nil, m.Err } mimes := make(map[string]string, len(m.Data)) for h, a := range m.Data { mimes[h] = a.Mime } return mimes, nil } func (m *MockArtworkRepo) PurgeDanglingItems(context.Context) (int64, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return 0, m.Err } var purged int64 for k, ia := range m.ItemData { // A nil per-kind map means that kind isn't tracked by the test, so keep it. existing := m.ExistingIDs[ia.ItemKind] if existing == nil { continue } if !existing[ia.ItemID] { delete(m.ItemData, k) purged++ } } return purged, nil } func (m *MockArtworkRepo) PurgeOrphans(_ context.Context, createdBefore time.Time) (int64, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return 0, m.Err } var deleted int64 for h, a := range m.Data { if m.referenced(h) || !a.CreatedAt.Before(createdBefore) { continue } delete(m.Data, h) deleted++ } return deleted, nil } func (m *MockArtworkRepo) referenced(hash string) bool { for _, ia := range m.ItemData { if ia.Hash == hash { return true } } return false } func (m *MockArtworkRepo) GetItemArtwork(_ context.Context, kind model.Kind, id, imageType string) (*model.ItemArtwork, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return nil, m.Err } if ia, ok := m.ItemData[iaKey(kind.Prefix(), id, imageType)]; ok { return &ia, nil } return nil, model.ErrNotFound } func (m *MockArtworkRepo) PutLastFailure(_ context.Context, kind model.Kind, id, imageType, trace string) error { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return m.Err } key := iaKey(kind.Prefix(), id, imageType) if ia, ok := m.ItemData[key]; ok { ia.LastFailure = trace m.ItemData[key] = ia } return nil } func (m *MockArtworkRepo) PutItemArtwork(_ context.Context, ia *model.ItemArtwork) error { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return m.Err } if ia.ImageType == "" { ia.ImageType = model.ImageTypePrimary } ia.UpdatedAt = time.Now() if ia.AttemptedAt.IsZero() { ia.AttemptedAt = ia.UpdatedAt } m.ItemData[iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)] = *ia return nil } func (m *MockArtworkRepo) DeleteForItems(_ context.Context, kind model.Kind, ids []string) error { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return m.Err } idSet := slice.ToSet(ids) maps.DeleteFunc(m.ItemData, func(_ string, ia model.ItemArtwork) bool { _, ok := idSet[ia.ItemID] return ok && ia.ItemKind == kind.Prefix() }) return nil } func (m *MockArtworkRepo) GetInfoForItems(_ context.Context, kind model.Kind, ids []string) (map[string]model.ItemArtworkInfo, error) { m.mu.Lock() defer m.mu.Unlock() if m.Err != nil { return nil, m.Err } res := map[string]model.ItemArtworkInfo{} for _, id := range ids { if ia, ok := m.ItemData[iaKey(kind.Prefix(), id, model.ImageTypePrimary)]; ok { info := model.ItemArtworkInfo{ItemID: id, Hash: ia.Hash} if a, ok := m.Data[ia.Hash]; ok { info.BlurHash, info.ThumbHash = a.BlurHash, a.ThumbHash info.DominantColor = a.DominantColor info.Width, info.Height = a.Width, a.Height } res[id] = info } } return res, nil }