test: add comprehensive test coverage for API endpoints and services
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheduler_NewScheduler(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
func TestScheduler_UpdateScanSettings(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
func TestScheduler_ConcurrentAccess(t *testing.T) {
|
||||
worker := NewWorker(1)
|
||||
scheduler := NewScheduler(worker, nil)
|
||||
scheduler.Start()
|
||||
defer scheduler.Stop()
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
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) {
|
||||
worker := NewWorker(1)
|
||||
defer worker.Shutdown()
|
||||
|
||||
// Fill the queue (capacity is 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
job := &Job{
|
||||
ID: fmt.Sprintf("job-%d", i),
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
worker.jobQueue <- job
|
||||
}
|
||||
|
||||
// Try to enqueue one more job
|
||||
job := &Job{
|
||||
ID: "overflow-job",
|
||||
Type: JobTypeScan,
|
||||
Params: map[string]interface{}{},
|
||||
Status: JobStatusPending,
|
||||
}
|
||||
|
||||
err := worker.EnqueueJob(job)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "job queue is full")
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
result, err := worker.processScanJob(job)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown job type")
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user