package scheduler import ( "context" "errors" "sync/atomic" "testing" "time" ) func TestNewScheduler(t *testing.T) { callCount := int32(0) crawlFunc := func(ctx context.Context) error { atomic.AddInt32(&callCount, 1) return nil } cfg := Config{ Enabled: true, Interval: 1 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) if scheduler == nil { t.Fatal("Expected non-nil scheduler") } if !scheduler.enabled { t.Error("Expected scheduler to be enabled") } if scheduler.interval != 1*time.Hour { t.Errorf("Expected interval 1h, got %v", scheduler.interval) } } func TestScheduler_Disabled(t *testing.T) { callCount := int32(0) crawlFunc := func(ctx context.Context) error { atomic.AddInt32(&callCount, 1) return nil } cfg := Config{ Enabled: false, Interval: 1 * time.Second, } scheduler := NewScheduler(cfg, crawlFunc) scheduler.Start() // Wait a bit - crawl should not run time.Sleep(100 * time.Millisecond) if atomic.LoadInt32(&callCount) != 0 { t.Error("Crawl should not run when scheduler is disabled") } } func TestScheduler_TriggerCrawl(t *testing.T) { callCount := int32(0) crawlFunc := func(ctx context.Context) error { atomic.AddInt32(&callCount, 1) time.Sleep(50 * time.Millisecond) // Simulate work return nil } cfg := Config{ Enabled: false, // Disabled scheduler, but manual trigger should work Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) // Trigger manual crawl err := scheduler.TriggerCrawl() if err != nil { t.Fatalf("TriggerCrawl failed: %v", err) } // Wait for crawl to complete time.Sleep(100 * time.Millisecond) if atomic.LoadInt32(&callCount) != 1 { t.Errorf("Expected 1 crawl, got %d", atomic.LoadInt32(&callCount)) } } func TestScheduler_TriggerCrawl_AlreadyRunning(t *testing.T) { crawlFunc := func(ctx context.Context) error { time.Sleep(200 * time.Millisecond) return nil } cfg := Config{ Enabled: false, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) // First trigger err := scheduler.TriggerCrawl() if err != nil { t.Fatalf("First TriggerCrawl failed: %v", err) } // Wait a bit for crawl to start time.Sleep(10 * time.Millisecond) // Second trigger should fail err = scheduler.TriggerCrawl() if err != ErrCrawlAlreadyRunning { t.Errorf("Expected ErrCrawlAlreadyRunning, got %v", err) } // Wait for crawl to complete time.Sleep(250 * time.Millisecond) // Now trigger should work again err = scheduler.TriggerCrawl() if err != nil { t.Errorf("Third TriggerCrawl should succeed: %v", err) } } func TestScheduler_Status(t *testing.T) { crawlFunc := func(ctx context.Context) error { return nil } cfg := Config{ Enabled: true, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) status := scheduler.Status() if !status.Enabled { t.Error("Expected enabled=true") } if status.Running { t.Error("Expected running=false initially") } if status.Interval != "24h0m0s" { t.Errorf("Expected interval '24h0m0s', got '%s'", status.Interval) } } func TestScheduler_Status_AfterCrawl(t *testing.T) { crawlFunc := func(ctx context.Context) error { return nil } cfg := Config{ Enabled: false, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) // Trigger and wait scheduler.TriggerCrawl() time.Sleep(50 * time.Millisecond) status := scheduler.Status() if status.LastRun.IsZero() { t.Error("Expected LastRun to be set") } if status.LastRunStatus != "success" { t.Errorf("Expected status 'success', got '%s'", status.LastRunStatus) } } func TestScheduler_Status_FailedCrawl(t *testing.T) { crawlFunc := func(ctx context.Context) error { return errors.New("connection failed") } cfg := Config{ Enabled: false, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) // Trigger and wait scheduler.TriggerCrawl() time.Sleep(50 * time.Millisecond) status := scheduler.Status() if status.LastRunStatus != "failed: connection failed" { t.Errorf("Expected failed status, got '%s'", status.LastRunStatus) } } func TestScheduler_IsRunning(t *testing.T) { crawlFunc := func(ctx context.Context) error { time.Sleep(100 * time.Millisecond) return nil } cfg := Config{ Enabled: false, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) if scheduler.IsRunning() { t.Error("Should not be running initially") } scheduler.TriggerCrawl() time.Sleep(10 * time.Millisecond) if !scheduler.IsRunning() { t.Error("Should be running after trigger") } time.Sleep(150 * time.Millisecond) if scheduler.IsRunning() { t.Error("Should not be running after completion") } } func TestScheduler_CalculateNextRun_Daily(t *testing.T) { crawlFunc := func(ctx context.Context) error { return nil } cfg := Config{ Enabled: true, Interval: 24 * time.Hour, } scheduler := NewScheduler(cfg, crawlFunc) // Test at 1 AM - should schedule for 2 AM same day from := time.Date(2024, 1, 15, 1, 0, 0, 0, time.UTC) next := scheduler.calculateNextRun(from) expectedHour := 2 if next.Hour() != expectedHour { t.Errorf("Expected hour %d, got %d", expectedHour, next.Hour()) } if next.Day() != 15 { t.Errorf("Expected day 15, got %d", next.Day()) } // Test at 3 AM - should schedule for 2 AM next day from = time.Date(2024, 1, 15, 3, 0, 0, 0, time.UTC) next = scheduler.calculateNextRun(from) if next.Day() != 16 { t.Errorf("Expected day 16, got %d", next.Day()) } } func TestScheduler_CalculateNextRun_Hourly(t *testing.T) { crawlFunc := func(ctx context.Context) error { return nil } cfg := Config{ Enabled: true, Interval: 1 * time.Hour, // Less than 24h } scheduler := NewScheduler(cfg, crawlFunc) from := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC) next := scheduler.calculateNextRun(from) // Should start in about 1 minute diff := next.Sub(from) if diff < 30*time.Second || diff > 90*time.Second { t.Errorf("Expected ~1 minute delay for short intervals, got %v", diff) } } func TestSchedulerError(t *testing.T) { err := ErrCrawlAlreadyRunning if err.Error() != "crawl already running" { t.Errorf("Unexpected error message: %s", err.Error()) } }