Files
bookhoard/cmd/server/tests/worker_test.go
T
john-okeefe b3b40b77d6 fix: replace fixed sleep with proper job polling in TestWorker_ConcurrentJobs
Problem:
TestWorker_ConcurrentJobs was using a fixed 3-second sleep to wait for
concurrent scan jobs to complete. However, this wasn't sufficient time
for the watch mode to enqueue and process the jobs. When the test function
ended, Go's testing framework deleted all t.TempDir() directories,
causing the scanner to fail with 'no such file or directory' errors.

Error messages:
  Processing media file: /tmp/.../002/book0.epub
  Failed to get file info for /tmp/.../002/book0.epub: stat ...: no such file or directory

Root Cause:
The test created temporary directories and files using t.TempDir(), which
are automatically cleaned up when the test function ends. The scanner
needs time to process the files, but the test only waited 3 seconds before
checking results, causing temp dirs to be deleted mid-scan.

Solution:
Replaced the fixed 3-second sleep with proper job polling that:
1. Stores job IDs when submitting them to the worker
2. Polls job status every 100ms up to a 15-second timeout
3. Waits until all 3 jobs reach Completed or Failed status
4. Only then checks for media items in the database

This ensures the scanner has finished processing all files before the test
ends and temp dirs are cleaned up. Matches the polling pattern used in
TestWorker_DirectoryScanJob.

Files changed:
- cmd/server/tests/worker_test.go: Added job tracking and proper polling
2026-03-24 21:15:40 -04:00

340 lines
9.6 KiB
Go

package main
import (
"bookhoard/internal/database"
"bookhoard/internal/services"
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWorker_DirectoryScanJob(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
// Create library
createLibReq := map[string]interface{}{
"name": "Worker Test Library",
"description": "Test library for worker",
"type": "ebooks",
}
createLibURL := setup.Server.URL + "/api/libraries"
createLibReqHTTP, _ := http.NewRequest("POST", createLibURL, bytes.NewBuffer(jsonMarshal(createLibReq)))
createLibReqHTTP.Header.Set("Content-Type", "application/json")
createLibReqHTTP.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
createLibResp, err := client.Do(createLibReqHTTP)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, createLibResp.StatusCode)
var createLibResponse map[string]interface{}
json.NewDecoder(createLibResp.Body).Decode(&createLibResponse)
createLibResp.Body.Close()
libraryID, ok := createLibResponse["id"].(string)
require.True(t, ok)
require.NotEmpty(t, libraryID)
// Create a temporary directory for testing
tmpDir := t.TempDir()
// Add folder to library
folderURL := fmt.Sprintf("%s/api/libraries/%s/folders", setup.Server.URL, libraryID)
folderReq := map[string]interface{}{
"folder_path": tmpDir,
}
req, _ := http.NewRequest("POST", folderURL, bytes.NewBuffer(jsonMarshal(folderReq)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusCreated, resp.StatusCode)
// Create test files in the directory
for i := 0; i < 3; i++ {
filePath := filepath.Join(tmpDir, fmt.Sprintf("book%d.epub", i))
err := os.WriteFile(filePath, []byte(fmt.Sprintf("test content %d", i)), 0644)
require.NoError(t, err)
}
// Submit directory scan job through WorkerInstance
job := &services.Job{
ID: uuid.New().String(),
Type: services.JobTypeDirectoryScan,
Params: map[string]interface{}{
"directory": tmpDir,
"db": setup.DB,
},
Status: services.JobStatusPending,
}
// Use EnqueueJob which returns errors
err = services.WorkerInstance.EnqueueJob(job)
require.NoError(t, err, "Failed to enqueue job")
// Poll for job completion
timeout := time.Now().Add(10 * time.Second)
for time.Now().Before(timeout) {
if status, exists := services.WorkerInstance.GetJobStatus(job.ID); exists {
if status.Status == services.JobStatusCompleted || status.Status == services.JobStatusFailed {
break
}
}
time.Sleep(100 * time.Millisecond)
}
// Debug: Check job status after polling
if finalStatus, exists := services.WorkerInstance.GetJobStatus(job.ID); exists {
t.Logf("Job %s final status: %s, error: %s", job.ID, finalStatus.Status, finalStatus.Error)
} else {
t.Logf("Job %s not found in results", job.ID)
}
// Check that items were created in database
ctx := context.Background()
items, err := setup.DB.ListMediaItems(ctx, database.ListMediaItemsParams{
Limit: 1000,
Offset: 0,
})
require.NoError(t, err)
// Should have at least 3 items (our test files)
greaterOrEqual := func(a, b int) bool {
return a >= b
}
assert.True(t, greaterOrEqual(len(items), 3), fmt.Sprintf("Expected at least 3 items, got %d", len(items)))
}
func TestWorker_SetFoldersJob(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
// Create library
createLibReq := map[string]interface{}{
"name": "Set Folders Test Library",
"description": "Test library for set folders",
"type": "ebooks",
}
createLibURL := setup.Server.URL + "/api/libraries"
createLibReqHTTP, _ := http.NewRequest("POST", createLibURL, bytes.NewBuffer(jsonMarshal(createLibReq)))
createLibReqHTTP.Header.Set("Content-Type", "application/json")
createLibReqHTTP.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
createLibResp, err := client.Do(createLibReqHTTP)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, createLibResp.StatusCode)
var createLibResponse map[string]interface{}
json.NewDecoder(createLibResp.Body).Decode(&createLibResponse)
createLibResp.Body.Close()
libraryID, ok := createLibResponse["id"].(string)
require.True(t, ok)
require.NotEmpty(t, libraryID)
// Create temporary directory
tmpDir := t.TempDir()
// Add folder to library via HTTP API
addFolderToLibrary(t, setup, libraryID, tmpDir)
// Submit set folders job through WorkerInstance
job := &services.Job{
ID: uuid.New().String(),
Type: services.JobTypeSetFolders,
Params: map[string]interface{}{
"folders": []string{tmpDir},
"db": setup.DB,
},
Status: services.JobStatusPending,
}
err = services.WorkerInstance.EnqueueJob(job)
require.NoError(t, err, "Failed to enqueue set folders job")
// Wait for job to process
time.Sleep(1 * time.Second)
// Verify folder was added to library
ctx := context.Background()
folders, err := setup.DB.GetLibraryFolders(ctx, pgtype.UUID{Bytes: [16]byte(uuid.MustParse(libraryID)), Valid: true})
require.NoError(t, err)
assert.NotEmpty(t, folders, "Library should have folders")
// Find our folder
found := false
for _, folder := range folders {
if folder.FolderPath == tmpDir {
found = true
break
}
}
assert.True(t, found, "Folder should be in library")
}
func TestWorker_ConcurrentJobs(t *testing.T) {
setup := setupTestServer(t)
ctx := context.Background()
// Create a library and get admin user
adminID := getTestUserID(t, setup.DB)
libraryType, err := setup.DB.GetLibraryTypeByName(ctx, "ebooks")
require.NoError(t, err)
library, err := setup.DB.CreateLibrary(ctx, database.CreateLibraryParams{
Name: "Concurrent Job Test Library",
Description: pgtype.Text{String: "Test library", Valid: true},
LibraryTypeID: libraryType.ID,
CreatedByAdminID: pgtype.UUID{Bytes: [16]byte(adminID), Valid: true},
})
require.NoError(t, err)
// Create multiple temporary directories
var tmpDirs []string
for i := 0; i < 3; i++ {
tmpDir := t.TempDir()
tmpDirs = append(tmpDirs, tmpDir)
// Add folder to library
_, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{
LibraryID: library.ID,
FolderPath: tmpDir,
})
require.NoError(t, err)
// Create test files in each directory
for j := 0; j < 2; j++ {
filePath := filepath.Join(tmpDir, fmt.Sprintf("book%d.epub", j))
err = os.WriteFile(filePath, []byte(fmt.Sprintf("test content %d", j)), 0644)
require.NoError(t, err, "Failed to create test file")
}
}
// Submit multiple concurrent jobs and track their IDs
var jobs []*services.Job
for _, tmpDir := range tmpDirs {
job := &services.Job{
ID: uuid.New().String(),
Type: services.JobTypeDirectoryScan,
Params: map[string]interface{}{
"directory": tmpDir,
"db": setup.DB,
},
Status: services.JobStatusPending,
}
jobs = append(jobs, job)
err = services.WorkerInstance.EnqueueJob(job)
require.NoError(t, err, "Failed to enqueue scan job")
}
// Wait for all jobs to complete
timeout := time.Now().Add(15 * time.Second)
completedJobs := 0
for time.Now().Before(timeout) && completedJobs < 3 {
for _, job := range jobs {
if status, exists := services.WorkerInstance.GetJobStatus(job.ID); exists {
if status.Status == services.JobStatusCompleted || status.Status == services.JobStatusFailed {
completedJobs++
}
}
}
if completedJobs < 3 {
time.Sleep(100 * time.Millisecond)
}
}
// Verify all directories were processed
allItems, err := setup.DB.ListMediaItems(ctx, database.ListMediaItemsParams{
Limit: 1000,
Offset: 0,
})
require.NoError(t, err)
assert.NotEmpty(t, allItems, "Should have media items from concurrent jobs")
}
func TestWorker_JobStatusTracking(t *testing.T) {
setup := setupTestServer(t)
ctx := context.Background()
// Create test library
adminID := getTestUserID(t, setup.DB)
libraryType, err := setup.DB.GetLibraryTypeByName(ctx, "ebooks")
require.NoError(t, err)
library, err := setup.DB.CreateLibrary(ctx, database.CreateLibraryParams{
Name: "Job Status Test Library",
Description: pgtype.Text{String: "Test library", Valid: true},
LibraryTypeID: libraryType.ID,
CreatedByAdminID: pgtype.UUID{Bytes: [16]byte(adminID), Valid: true},
})
require.NoError(t, err)
tmpDir := t.TempDir()
_, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{
LibraryID: library.ID,
FolderPath: tmpDir,
})
require.NoError(t, err)
// Submit job and track status
jobID := uuid.New().String()
job := &services.Job{
ID: jobID,
Type: services.JobTypeDirectoryScan,
Params: map[string]interface{}{
"directory": tmpDir,
"db": setup.DB,
},
Status: services.JobStatusPending,
}
err = services.WorkerInstance.EnqueueJob(job)
require.NoError(t, err, "Failed to enqueue set folders job")
// Poll job status
var finalStatus string
for i := 0; i < 10; i++ {
time.Sleep(500 * time.Millisecond)
result, exists := services.WorkerInstance.GetJobStatus(jobID)
if exists {
finalStatus = string(result.Status)
if result.Status == services.JobStatusCompleted || result.Status == services.JobStatusFailed {
break
}
}
}
// Job should complete (success or failure)
assert.True(t, finalStatus == "completed" || finalStatus == "failed",
fmt.Sprintf("Job should complete, got status: %s", finalStatus))
}
// Helper function to marshal JSON
func jsonMarshal(v interface{}) []byte {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
return b
}