navidrome/tests/mock_playlist_repo.go
Deluan 9c516cf601 refactor(plugins): serialize PlaylistGenerator plugin calls via work queue
Replace the timer-per-playlist + WaitGroup.Go model with a single worker
goroutine processing work items from a buffered channel. This eliminates
race conditions from parallel plugin calls (rate limiting risk),
unsynchronized map/field access across goroutines, and overlapping
discovery and refresh timers. The worker owns all mutable state
(refreshTimers, discoveryTimer) exclusively, while retryInterval and
refreshTimerCount use atomics for safe test observability. Also adds
retry-on-failure for GetAvailablePlaylists (5 min delay) and makes
MockPlaylistRepo thread-safe with sync.RWMutex.
2026-04-12 17:32:18 -04:00

159 lines
3.3 KiB
Go

package tests
import (
"errors"
"sync"
"github.com/deluan/rest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/id"
)
func CreateMockPlaylistRepo() *MockPlaylistRepo {
return &MockPlaylistRepo{
Data: make(map[string]*model.Playlist),
PathMap: make(map[string]*model.Playlist),
}
}
type MockPlaylistRepo struct {
model.PlaylistRepository
mu sync.RWMutex
Data map[string]*model.Playlist // keyed by ID
PathMap map[string]*model.Playlist // keyed by path
Last *model.Playlist
Deleted []string
Err bool
TracksRepo model.PlaylistTrackRepository
}
func (m *MockPlaylistRepo) SetError(err bool) {
m.mu.Lock()
defer m.mu.Unlock()
m.Err = err
}
func (m *MockPlaylistRepo) Get(id string) (*model.Playlist, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.Err {
return nil, errors.New("error")
}
if m.Data != nil {
if pls, ok := m.Data[id]; ok {
return pls, nil
}
}
return nil, model.ErrNotFound
}
func (m *MockPlaylistRepo) GetWithTracks(id string, _, _ bool) (*model.Playlist, error) {
return m.Get(id)
}
func (m *MockPlaylistRepo) Put(pls *model.Playlist) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err {
return errors.New("error")
}
if pls.ID == "" {
pls.ID = id.NewRandom()
}
m.Last = pls
if m.Data != nil {
m.Data[pls.ID] = pls
}
return nil
}
func (m *MockPlaylistRepo) FindByPath(path string) (*model.Playlist, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.Err {
return nil, errors.New("error")
}
if m.PathMap != nil {
if pls, ok := m.PathMap[path]; ok {
return pls, nil
}
}
return nil, model.ErrNotFound
}
func (m *MockPlaylistRepo) Delete(id string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err {
return errors.New("error")
}
m.Deleted = append(m.Deleted, id)
return nil
}
func (m *MockPlaylistRepo) Tracks(_ string, _ bool) model.PlaylistTrackRepository {
m.mu.RLock()
defer m.mu.RUnlock()
return m.TracksRepo
}
func (m *MockPlaylistRepo) Exists(id string) (bool, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.Err {
return false, errors.New("error")
}
if m.Data != nil {
_, found := m.Data[id]
return found, nil
}
return false, nil
}
func (m *MockPlaylistRepo) Count(_ ...rest.QueryOptions) (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.Err {
return 0, errors.New("error")
}
return int64(len(m.Data)), nil
}
func (m *MockPlaylistRepo) CountAll(_ ...model.QueryOptions) (int64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.Err {
return 0, errors.New("error")
}
return int64(len(m.Data)), nil
}
// Len returns the number of playlists in the repo in a thread-safe manner.
func (m *MockPlaylistRepo) Len() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.Data)
}
// GetData returns a playlist by ID in a thread-safe manner.
func (m *MockPlaylistRepo) GetData(id string) (*model.Playlist, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
pls, ok := m.Data[id]
return pls, ok
}
// FindByPluginPlaylistID returns the first playlist with the given plugin playlist ID.
func (m *MockPlaylistRepo) FindByPluginPlaylistID(pluginPlaylistID string) *model.Playlist {
m.mu.RLock()
defer m.mu.RUnlock()
for _, pls := range m.Data {
if pls.PluginPlaylistID == pluginPlaylistID {
return pls
}
}
return nil
}
var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)