navidrome/tests/mock_scrobble_buffer_repo.go
Deluan Quintão f48943c058
fix(plugins): discard buffered scrobbles when a plugin is removed (#5737)
* fix(plugins): discard buffered scrobbles when a plugin is removed

Scrobbles are buffered in the DB per service, keyed by the plugin name.
When a plugin was removed (deleted from the plugins folder and detected by
the sync), its pending buffer entries were left behind forever: the drain
goroutine is stopped on the next scrobbler refresh, so the rows were never
retried nor discarded. Worse, if a plugin with the same name was installed
later, the stale entries would be drained into it - potentially a completely
unrelated plugin that just reuses the name.

Add a Discard(service) method to ScrobbleBufferRepository and call it from
removePluginFromDB, right after the plugin record is deleted. Disabling a
plugin intentionally keeps its buffered scrobbles, consistent with the
buffer's purpose of surviving temporary outages, and transient unload/reload
cycles during config updates are unaffected since they never delete the
plugin record.

* fix(plugins): don't wipe builtin scrobbler queues on plugin removal

Buffer entries are keyed by service name only, and removePluginFromDB runs
for any removed plugin file, so removing a plugin named e.g. lastfm.ndp -
regardless of its capability - would discard the builtin Last.fm retry
queue. Skip the discard when the plugin name is owned by a registered
builtin scrobbler, exposed via a new scrobbler.IsBuiltinScrobbler helper.
Reported by Codex review on the PR.

Also drop the testBroker usage from the new removePluginFromDB spec: it is
defined in manager_test.go which is excluded on Windows, breaking the
Windows test build. sendPluginRefreshEvent is nil-safe, so no broker is
needed.
2026-07-08 12:37:17 -04:00

110 lines
2.3 KiB
Go

package tests
import (
"sync"
"time"
"github.com/navidrome/navidrome/model"
)
type MockedScrobbleBufferRepo struct {
Error error
Data model.ScrobbleEntries
mu sync.RWMutex
}
func CreateMockedScrobbleBufferRepo() *MockedScrobbleBufferRepo {
return &MockedScrobbleBufferRepo{}
}
func (m *MockedScrobbleBufferRepo) UserIDs(service string) ([]string, error) {
if m.Error != nil {
return nil, m.Error
}
m.mu.RLock()
defer m.mu.RUnlock()
userIds := make(map[string]struct{})
for _, e := range m.Data {
if e.Service == service {
userIds[e.UserID] = struct{}{}
}
}
var result []string
for uid := range userIds {
result = append(result, uid)
}
return result, nil
}
func (m *MockedScrobbleBufferRepo) Enqueue(service, userId, mediaFileId string, playTime time.Time) error {
if m.Error != nil {
return m.Error
}
m.mu.Lock()
defer m.mu.Unlock()
m.Data = append(m.Data, model.ScrobbleEntry{
MediaFile: model.MediaFile{ID: mediaFileId},
Service: service,
UserID: userId,
PlayTime: playTime,
EnqueueTime: time.Now(),
})
return nil
}
func (m *MockedScrobbleBufferRepo) Next(service, userId string) (*model.ScrobbleEntry, error) {
if m.Error != nil {
return nil, m.Error
}
m.mu.RLock()
defer m.mu.RUnlock()
for _, e := range m.Data {
if e.Service == service && e.UserID == userId {
return &e, nil
}
}
return nil, nil
}
func (m *MockedScrobbleBufferRepo) Dequeue(entry *model.ScrobbleEntry) error {
if m.Error != nil {
return m.Error
}
m.mu.Lock()
defer m.mu.Unlock()
newData := model.ScrobbleEntries{}
for _, e := range m.Data {
if e.Service == entry.Service && e.UserID == entry.UserID && e.PlayTime == entry.PlayTime && e.MediaFile.ID == entry.MediaFile.ID {
continue
}
newData = append(newData, e)
}
m.Data = newData
return nil
}
func (m *MockedScrobbleBufferRepo) Discard(service string) error {
if m.Error != nil {
return m.Error
}
m.mu.Lock()
defer m.mu.Unlock()
newData := model.ScrobbleEntries{}
for _, e := range m.Data {
if e.Service != service {
newData = append(newData, e)
}
}
m.Data = newData
return nil
}
func (m *MockedScrobbleBufferRepo) Length() (int64, error) {
if m.Error != nil {
return 0, m.Error
}
m.mu.RLock()
defer m.mu.RUnlock()
return int64(len(m.Data)), nil
}