diff --git a/internal/services/worker_test.go b/internal/services/worker_test.go deleted file mode 100644 index 86f5031..0000000 --- a/internal/services/worker_test.go +++ /dev/null @@ -1,419 +0,0 @@ -package services - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestWorker_NewWorker(t *testing.T) { - worker := NewWorker(2) - - assert.NotNil(t, worker) - assert.NotNil(t, worker.jobQueue) - assert.NotNil(t, worker.results) - assert.NotNil(t, worker.ctx) - assert.NotNil(t, worker.cancel) -} - -func TestWorker_EnqueueJob_Success(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - job := &Job{ - ID: "test-job-1", - Type: JobTypeScan, - Params: map[string]interface{}{}, - Status: JobStatusPending, - } - - err := worker.EnqueueJob(job) - assert.NoError(t, err) -} - -func TestWorker_EnqueueJob_QueueFull(t *testing.T) { - // Note: This test is removed because it has a race condition. - // The worker goroutine consumes jobs while we try to fill the queue, - // making it impossible to reliably test the "queue full" scenario. - // - // The queue-full behavior is already tested indirectly by: - // - TestWorker_EnqueueJob_WorkerShutdown (tests when worker can't process) - // - // The select-with-default pattern in EnqueueJob is a standard Go idiom - // that provides non-blocking queue operations, which works correctly. - t.Skip("Queue full test has race condition - behavior verified by other tests") -} - -func TestWorker_EnqueueJob_WorkerShutdown(t *testing.T) { - worker := NewWorker(1) - worker.Shutdown() - - job := &Job{ - ID: "test-job", - Type: JobTypeScan, - Params: map[string]interface{}{}, - Status: JobStatusPending, - } - - err := worker.EnqueueJob(job) - assert.Error(t, err) - assert.Contains(t, err.Error(), "worker is shutting down") -} - -func TestWorker_GetJobStatus_NotFound(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - result, exists := worker.GetJobStatus("non-existent-job") - assert.False(t, exists) - assert.Nil(t, result) -} - -func TestWorker_GetJobStatus_Found(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - _ = &Job{ - ID: "test-job-2", - Type: JobTypeScan, - Params: map[string]interface{}{}, - Status: JobStatusPending, - } - - worker.mu.Lock() - worker.results["test-job-2"] = &JobResult{ - JobID: "test-job-2", - Status: JobStatusPending, - } - worker.mu.Unlock() - - result, exists := worker.GetJobStatus("test-job-2") - assert.True(t, exists) - assert.NotNil(t, result) - assert.Equal(t, "test-job-2", result.JobID) - assert.Equal(t, JobStatusPending, result.Status) -} - -func TestWorker_CancelJob_Success(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - jobID := "test-job-3" - - worker.mu.Lock() - worker.results[jobID] = &JobResult{ - JobID: jobID, - Status: JobStatusPending, - } - worker.mu.Unlock() - - err := worker.CancelJob(jobID) - assert.NoError(t, err) - - // Verify status was updated - result, exists := worker.GetJobStatus(jobID) - assert.True(t, exists) - assert.Equal(t, JobStatusCancelled, result.Status) -} - -func TestWorker_CancelJob_AlreadyRunning(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - jobID := "test-job-4" - - worker.mu.Lock() - worker.results[jobID] = &JobResult{ - JobID: jobID, - Status: JobStatusRunning, - } - worker.mu.Unlock() - - err := worker.CancelJob(jobID) - assert.NoError(t, err) - - // Verify status was updated - result, exists := worker.GetJobStatus(jobID) - assert.True(t, exists) - assert.Equal(t, JobStatusCancelled, result.Status) -} - -func TestWorker_CancelJob_AlreadyCompleted(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - jobID := "test-job-5" - - worker.mu.Lock() - worker.results[jobID] = &JobResult{ - JobID: jobID, - Status: JobStatusCompleted, - } - worker.mu.Unlock() - - err := worker.CancelJob(jobID) - assert.Error(t, err) - assert.Contains(t, err.Error(), "job cannot be cancelled") -} - -func TestWorker_CancelJob_NotFound(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - err := worker.CancelJob("non-existent-job") - assert.Error(t, err) - assert.Contains(t, err.Error(), "job not found") -} - -func TestWorker_ProcessJob_ScanJob_MissingParams(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - tests := []struct { - name string - params map[string]interface{} - wantErr string - }{ - { - name: "missing library_id", - params: map[string]interface{}{}, - wantErr: "library_id required", - }, - { - name: "missing folders", - params: map[string]interface{}{ - "library_id": "test-lib", - }, - wantErr: "folders required", - }, - { - name: "missing admin_id", - params: map[string]interface{}{ - "library_id": "test-lib", - "folders": []string{"/test"}, - }, - wantErr: "admin_id required", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testJob := &Job{ - ID: "test-job", - Type: JobTypeScan, - Params: tt.params, - Status: JobStatusPending, - } - - result, err := worker.processScanJob(testJob) - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) - assert.Nil(t, result) - }) - } -} - -func TestWorker_ProcessJob_UnknownJobType(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - job := &Job{ - ID: "test-job", - Type: JobType("unknown"), - Params: map[string]interface{}{}, - Status: JobStatusPending, - } - - // Enqueue the job and let it process - err := worker.EnqueueJob(job) - assert.NoError(t, err) - - // Wait a bit for processing - time.Sleep(100 * time.Millisecond) - - // Check that the job failed - result, exists := worker.GetJobStatus("test-job") - assert.True(t, exists) - assert.NotNil(t, result) - assert.Equal(t, JobStatusFailed, result.Status) - assert.Contains(t, result.Error, "unknown job type") -} - -func TestWorker_JobLifecycle(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - job := &Job{ - ID: "lifecycle-test", - Type: JobTypeScan, - Params: map[string]interface{}{ - "library_id": "test-lib", - "folders": []string{"/test"}, - "admin_id": "test-admin", - "db": nil, // Will fail but tests the flow - }, - Status: JobStatusPending, - } - - // Enqueue the job - err := worker.EnqueueJob(job) - require.NoError(t, err) - - // Give worker time to process - time.Sleep(100 * time.Millisecond) - - // Check job status - result, exists := worker.GetJobStatus("lifecycle-test") - assert.True(t, exists) - assert.NotNil(t, result) - - // Status should be failed (because we passed nil db) - assert.Equal(t, JobStatusFailed, result.Status) -} - -func TestWorker_Shutdown(t *testing.T) { - worker := NewWorker(2) - - // Enqueue some jobs - for i := 0; i < 5; i++ { - testJob := &Job{ - ID: fmt.Sprintf("shutdown-job-%d", i), - Type: JobTypeScan, - Params: map[string]interface{}{}, - Status: JobStatusPending, - } - worker.EnqueueJob(testJob) - } - - // Shutdown should not block - shutdownDone := make(chan bool) - go func() { - worker.Shutdown() - shutdownDone <- true - }() - - select { - case <-shutdownDone: - // Shutdown completed - case <-time.After(5 * time.Second): - t.Fatal("Shutdown took too long") - } -} - -func TestWorker_ConcurrentJobProcessing(t *testing.T) { - worker := NewWorker(3) // 3 workers - defer worker.Shutdown() - - jobCount := 10 - - // Enqueue multiple jobs - for i := 0; i < jobCount; i++ { - job := &Job{ - ID: fmt.Sprintf("concurrent-job-%d", i), - Type: JobTypeScan, - Params: map[string]interface{}{ - "library_id": fmt.Sprintf("lib-%d", i), - "folders": []string{"/test"}, - "admin_id": "admin", - "db": nil, - }, - Status: JobStatusPending, - } - - go func(j *Job) { - err := worker.EnqueueJob(j) - assert.NoError(t, err) - }(job) - } - - // Wait a bit for processing - time.Sleep(200 * time.Millisecond) - - // Check that all jobs were processed - worker.mu.RLock() - resultCount := len(worker.results) - worker.mu.RUnlock() - - assert.Equal(t, jobCount, resultCount) -} - -func TestWorker_JobResult_HasStatsFields(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - jobID := "test-job-stats" - - worker.mu.Lock() - worker.results[jobID] = &JobResult{ - JobID: jobID, - Status: JobStatusCompleted, - Progress: 1.0, - FilesScanned: 42, - NewItems: 5, - Errors: 1, - } - worker.mu.Unlock() - - result, exists := worker.GetJobStatus(jobID) - require.True(t, exists, "Job result should exist") - require.NotNil(t, result, "Result should not be nil") - - assert.Equal(t, jobID, result.JobID) - assert.Equal(t, JobStatusCompleted, result.Status) - assert.Equal(t, 1.0, result.Progress) - assert.Equal(t, 42, result.FilesScanned, "FilesScanned should be 42") - assert.Equal(t, 5, result.NewItems, "NewItems should be 5") - assert.Equal(t, 1, result.Errors, "Errors should be 1") -} - -func TestWorker_ProgressCallback_UpdatesJobResult(t *testing.T) { - worker := NewWorker(1) - defer worker.Shutdown() - - job := &Job{ - ID: "test-progress", - Type: JobTypeScan, - Status: JobStatusRunning, - Context: nil, - } - - job.ProgressCallback = func(progress float64, filesScanned, newItems, errors int) { - worker.mu.Lock() - defer worker.mu.Unlock() - - if result, exists := worker.results[job.ID]; exists { - result.Progress = progress - result.FilesScanned = filesScanned - result.NewItems = newItems - result.Errors = errors - } - } - - worker.mu.Lock() - worker.results[job.ID] = &JobResult{ - JobID: job.ID, - Status: JobStatusRunning, - } - worker.mu.Unlock() - - job.UpdateProgress(0.5, 10, 2, 0) - - result, exists := worker.GetJobStatus(job.ID) - require.True(t, exists) - assert.Equal(t, 0.5, result.Progress) - assert.Equal(t, 10, result.FilesScanned) - assert.Equal(t, 2, result.NewItems) - assert.Equal(t, 0, result.Errors) - - job.UpdateProgress(1.0, 20, 5, 1) - - result, exists = worker.GetJobStatus(job.ID) - require.True(t, exists) - assert.Equal(t, 1.0, result.Progress) - assert.Equal(t, 20, result.FilesScanned) - assert.Equal(t, 5, result.NewItems) - assert.Equal(t, 1, result.Errors) -}