package worker import ( "os" "path/filepath" "testing" "time" "vidarchive/internal/config" "vidarchive/internal/database" "vidarchive/internal/models" "vidarchive/internal/repository" "vidarchive/internal/service" ) // newTestPool builds a pool over a real service and database, with yt-dlp // replaced by a script that reports it has started and then blocks. scratch is // where that script writes its marker. func newTestPool(t *testing.T) (pool *Pool, repo *repository.DownloadRepository, scratch string) { t.Helper() root := t.TempDir() cfg := &config.Config{ DBPath: ":memory:", LibraryDir: filepath.Join(root, "library"), TempDir: filepath.Join(root, "temp"), FFmpegPath: "ffmpeg", FFprobePath: "ffprobe", } for _, dir := range []string{cfg.LibraryDir, cfg.TempDir} { if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } } // The script must not exit on its own: the point of these tests is that Stop // interrupts a download rather than waiting one out. cfg.YTDLPPath = filepath.Join(root, "yt-dlp") script := "#!/bin/sh\ntouch " + filepath.Join(root, "started") + "\nsleep 300\n" if err := os.WriteFile(cfg.YTDLPPath, []byte(script), 0755); err != nil { t.Fatal(err) } db, err := database.New(cfg) if err != nil { t.Fatalf("init db: %v", err) } t.Cleanup(func() { db.Close() }) repo = repository.NewDownloadRepository(db) downloadSvc := service.NewDownloadService( repo, service.NewLibraryService(cfg.LibraryDir, cfg.FFmpegPath, cfg.FFprobePath), service.NewPresetService(repository.NewPresetRepository(db)), service.NewSettingsService(repository.NewSettingsRepository(db)), service.NewSubscriptionService(repository.NewSubscriptionRepository(db), cfg), cfg, ) return New(downloadSvc, 2), repo, root } func waitForFile(t *testing.T, path string) { t.Helper() deadline := time.Now().Add(10 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(path); err == nil { return } time.Sleep(5 * time.Millisecond) } t.Fatalf("timed out waiting for %s", path) } // Stop must return while a download is still running. If it waited for yt-dlp // to finish on its own, a shutdown would hang for as long as the download takes. func TestStopReturnsWhileDownloadRunning(t *testing.T) { pool, repo, scratch := newTestPool(t) d := &models.Download{URL: "https://example.com/v", Status: "queued"} if err := repo.Create(d); err != nil { t.Fatal(err) } pool.Start() pool.Submit(d) waitForFile(t, filepath.Join(scratch, "started")) stopped := make(chan struct{}) go func() { pool.Stop() close(stopped) }() select { case <-stopped: case <-time.After(30 * time.Second): t.Fatal("Stop did not return while a download was running") } // The row stays "downloading" after a shutdown so the next start resumes it. got, err := repo.GetByID(d.ID) if err != nil { t.Fatal(err) } if got.Status != "downloading" { t.Errorf("status = %q, want downloading", got.Status) } } // Stop is called from the shutdown path and may race with an already-stopped // pool; calling it twice must not panic or block. func TestStopIsIdempotent(t *testing.T) { pool, _, _ := newTestPool(t) pool.Start() done := make(chan struct{}) go func() { pool.Stop() pool.Stop() close(done) }() select { case <-done: case <-time.After(30 * time.Second): t.Fatal("second Stop blocked") } } // A download submitted after shutdown must be dropped rather than sent to a // channel nobody is reading. func TestSubmitAfterStopIsNoOp(t *testing.T) { pool, repo, _ := newTestPool(t) pool.Start() pool.Stop() d := &models.Download{URL: "https://example.com/v", Status: "queued"} if err := repo.Create(d); err != nil { t.Fatal(err) } done := make(chan struct{}) go func() { pool.Submit(d) close(done) }() select { case <-done: case <-time.After(10 * time.Second): t.Fatal("Submit blocked after Stop") } if len(pool.queue) != 0 { t.Errorf("queue holds %d items, want 0 after Stop", len(pool.queue)) } } // Submit is called from an HTTP handler, so a full buffer must never block it: // the row stays queued in the database and the queue checker picks it up later. func TestSubmitDoesNotBlockWhenBufferFull(t *testing.T) { pool, _, _ := newTestPool(t) // Deliberately no Start: nothing drains the buffer. done := make(chan struct{}) go func() { for i := 0; i < cap(pool.queue)*2; i++ { pool.Submit(&models.Download{ID: int64(i + 1), URL: "https://example.com/v"}) } close(done) }() select { case <-done: case <-time.After(10 * time.Second): t.Fatal("Submit blocked on a full buffer") } if len(pool.queue) != cap(pool.queue) { t.Errorf("queue holds %d items, want the buffer to be full at %d", len(pool.queue), cap(pool.queue)) } }