mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-26 10:30:46 +00:00
* fix(api): return correct scanType in startScan response The startScan endpoint launches the scan in a goroutine and immediately calls GetScanStatus to build the response. Because the scanner hasn't had time to initialize and write its state to the database, the response contained stale data from the previous scan (e.g., scanType "quick" when fullScan=true was requested). Add a polling loop that waits briefly (up to 3s, polling every 50ms) for the scanner to report Scanning=true before returning the status. If the timeout expires, it falls back to the current behavior (no regression). Fixes #5158 * fix(api): use ticker/timer with context cancellation for scan polling Replace time.Sleep loop with proper ticker, timer, and ctx.Done() handling so the poll exits cleanly on timeout or client disconnect. * fix(api): handle fast scan completion in poll loop Add a channel to detect when the scan goroutine finishes before the poll loop observes Scanning=true, avoiding a 3s timeout on very fast scans. Use defer close to handle both success and error paths.
143 lines
3.4 KiB
Go
143 lines
3.4 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
// MockScanner implements scanner.Scanner for testing with proper synchronization
|
|
type MockScanner struct {
|
|
mu sync.Mutex
|
|
scanAllCalls []ScanAllCall
|
|
scanFoldersCalls []ScanFoldersCall
|
|
scanningStatus bool
|
|
statusResponse *model.ScannerStatus
|
|
scanStatusFunc func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus
|
|
}
|
|
|
|
type ScanAllCall struct {
|
|
FullScan bool
|
|
}
|
|
|
|
type ScanFoldersCall struct {
|
|
FullScan bool
|
|
Targets []model.ScanTarget
|
|
}
|
|
|
|
func NewMockScanner() *MockScanner {
|
|
return &MockScanner{
|
|
scanAllCalls: make([]ScanAllCall, 0),
|
|
scanFoldersCalls: make([]ScanFoldersCall, 0),
|
|
}
|
|
}
|
|
|
|
func (m *MockScanner) ScanAll(_ context.Context, fullScan bool) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
m.scanAllCalls = append(m.scanAllCalls, ScanAllCall{FullScan: fullScan})
|
|
|
|
// Simulate the scanner updating its status when the scan starts
|
|
if m.scanStatusFunc != nil {
|
|
m.statusResponse = m.scanStatusFunc(fullScan, nil)
|
|
} else {
|
|
m.scanningStatus = true
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockScanner) ScanFolders(_ context.Context, fullScan bool, targets []model.ScanTarget) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Make a copy of targets to avoid race conditions
|
|
targetsCopy := make([]model.ScanTarget, len(targets))
|
|
copy(targetsCopy, targets)
|
|
|
|
m.scanFoldersCalls = append(m.scanFoldersCalls, ScanFoldersCall{
|
|
FullScan: fullScan,
|
|
Targets: targetsCopy,
|
|
})
|
|
|
|
// Simulate the scanner updating its status when the scan starts
|
|
if m.scanStatusFunc != nil {
|
|
m.statusResponse = m.scanStatusFunc(fullScan, targetsCopy)
|
|
} else {
|
|
m.scanningStatus = true
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockScanner) Status(_ context.Context) (*model.ScannerStatus, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.statusResponse != nil {
|
|
return m.statusResponse, nil
|
|
}
|
|
|
|
return &model.ScannerStatus{
|
|
Scanning: m.scanningStatus,
|
|
}, nil
|
|
}
|
|
|
|
func (m *MockScanner) GetScanAllCallCount() int {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.scanAllCalls)
|
|
}
|
|
|
|
func (m *MockScanner) GetScanAllCalls() []ScanAllCall {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
// Return a copy to avoid race conditions
|
|
calls := make([]ScanAllCall, len(m.scanAllCalls))
|
|
copy(calls, m.scanAllCalls)
|
|
return calls
|
|
}
|
|
|
|
func (m *MockScanner) GetScanFoldersCallCount() int {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.scanFoldersCalls)
|
|
}
|
|
|
|
func (m *MockScanner) GetScanFoldersCalls() []ScanFoldersCall {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
// Return a copy to avoid race conditions
|
|
calls := make([]ScanFoldersCall, len(m.scanFoldersCalls))
|
|
copy(calls, m.scanFoldersCalls)
|
|
return calls
|
|
}
|
|
|
|
func (m *MockScanner) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.scanAllCalls = make([]ScanAllCall, 0)
|
|
m.scanFoldersCalls = make([]ScanFoldersCall, 0)
|
|
}
|
|
|
|
func (m *MockScanner) SetScanning(scanning bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.scanningStatus = scanning
|
|
}
|
|
|
|
func (m *MockScanner) SetStatusResponse(status *model.ScannerStatus) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.statusResponse = status
|
|
}
|
|
|
|
// SetScanStatusFunc sets a function that will be called when ScanAll/ScanFolders is invoked,
|
|
// simulating the scanner updating its status when the scan starts.
|
|
func (m *MockScanner) SetScanStatusFunc(fn func(fullScan bool, targets []model.ScanTarget) *model.ScannerStatus) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.scanStatusFunc = fn
|
|
}
|