Files
bookhoard/internal/services/scheduler_test.go
T
john-okeefe 70acecc33a Fix scheduler goroutine WaitGroup leak causing shutdown deadlock
Critical bug fix: The scheduler's runSettingsChecker() goroutine was
started but never marked as complete in the WaitGroup, causing
scheduler.Stop() to hang indefinitely waiting for wg.Wait().

Changes:
- Add defer s.wg.Done() call in scheduler.Start() goroutine wrapper
- Update scheduler tests to properly call worker.Shutdown()
- Add nil check for timer.Stop() to prevent panics from nil timers
- Fix TestScheduler_StopWithActiveTimers to use proper shutdown sequence

Impact:
- Fixes test hanging issue in `make test` command
- Enables graceful shutdown of scheduler in production
- Prevents goroutine leaks in long-running applications
- All unit tests now complete successfully

Root cause: WaitGroup.Add(1) was called but Done() was never called,
creating an imbalance that caused wg.Wait() to block forever.
2026-02-09 10:13:48 -05:00

153 lines
3.3 KiB
Go

package services
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestScheduler_NewScheduler(t *testing.T) {
worker := NewWorker(1)
defer worker.Shutdown()
// Use nil database interface for basic testing
scheduler := NewScheduler(worker, nil)
assert.NotNil(t, scheduler)
assert.NotNil(t, scheduler.worker)
assert.NotNil(t, scheduler.timers)
assert.NotNil(t, scheduler.scanSettings)
assert.NotNil(t, scheduler.ctx)
assert.NotNil(t, scheduler.cancel)
}
func TestScheduler_StartStop(t *testing.T) {
worker := NewWorker(1)
scheduler := NewScheduler(worker, nil)
// Start should not panic
scheduler.Start()
assert.NotNil(t, scheduler.ctx)
// Stop should not panic
scheduler.Stop()
worker.Shutdown()
}
func TestScheduler_UpdateScanSettings(t *testing.T) {
worker := NewWorker(1)
defer worker.Shutdown()
scheduler := NewScheduler(worker, nil)
userID := "test-user-123"
// Update scan settings
scheduler.UpdateScanSettings(userID, true, 30)
scheduler.mu.Lock()
settings, exists := scheduler.scanSettings[userID]
scheduler.mu.Unlock()
assert.True(t, exists)
assert.Equal(t, userID, settings.UserID)
assert.True(t, settings.Enabled)
assert.Equal(t, 30, settings.Frequency)
}
func TestScheduler_UpdateScanSettings_Disabled(t *testing.T) {
worker := NewWorker(1)
defer worker.Shutdown()
scheduler := NewScheduler(worker, nil)
userID := "test-user-456"
// Update scan settings to disabled
scheduler.UpdateScanSettings(userID, false, 60)
scheduler.mu.Lock()
settings, exists := scheduler.scanSettings[userID]
scheduler.mu.Unlock()
assert.True(t, exists)
assert.False(t, settings.Enabled)
assert.Equal(t, 60, settings.Frequency)
}
func TestScheduler_UpdateScanSettings_Overwrite(t *testing.T) {
worker := NewWorker(1)
defer worker.Shutdown()
scheduler := NewScheduler(worker, nil)
userID := "test-user-789"
// First update
scheduler.UpdateScanSettings(userID, true, 30)
// Overwrite with different settings
scheduler.UpdateScanSettings(userID, false, 45)
scheduler.mu.Lock()
settings, exists := scheduler.scanSettings[userID]
scheduler.mu.Unlock()
assert.True(t, exists)
assert.False(t, settings.Enabled)
assert.Equal(t, 45, settings.Frequency)
}
func TestScheduler_StopWithActiveTimers(t *testing.T) {
worker := NewWorker(1)
scheduler := NewScheduler(worker, nil)
// Add some fake timers
scheduler.mu.Lock()
scheduler.timers["timer1"] = nil
scheduler.timers["timer2"] = nil
scheduler.timers["timer3"] = nil
scheduler.mu.Unlock()
// Stop should clear timers
scheduler.Stop()
scheduler.mu.Lock()
timerCount := len(scheduler.timers)
scheduler.mu.Unlock()
assert.Equal(t, 0, timerCount)
worker.Shutdown()
}
func TestScheduler_ConcurrentAccess(t *testing.T) {
worker := NewWorker(1)
scheduler := NewScheduler(worker, nil)
scheduler.Start()
// Concurrent updates should not cause race conditions
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(index int) {
userID := uuid.New().String()
scheduler.UpdateScanSettings(userID, true, 30)
done <- true
}(i)
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
// Verify all settings were stored
scheduler.mu.Lock()
settingCount := len(scheduler.scanSettings)
scheduler.mu.Unlock()
assert.Equal(t, 10, settingCount)
scheduler.Stop()
worker.Shutdown()
}