Files
bookhoard/cmd/server/tests/worker_test.go
T
john-okeefe b700f64624 fix: remove redundant defer setup.Close() calls to enable library cleanup
Problem:
Tests were calling `defer setup.Close()` which was interfering with the
library cleanup added in the previous commit. The execution order was:

1. setupTestServer() registers t.Cleanup() with library deletion code
2. Test calls defer setup.Close()
3. Test finishes:
   - defer setup.Close() runs FIRST → closes DB pool
   - t.Cleanup() runs SECOND → tries to delete libraries but DB is closed!

This prevented "Job Status Test Library" and other test libraries from
being cleaned up, leaving residual data in the database after tests.

Root Cause:
The setupTestServer() function already handles cleanup via t.Cleanup(),
which calls setup.Close() at the end. The explicit defer calls were
redundant and caused the database pool to close before library cleanup
could execute.

Solution:
Removed all 17 occurrences of `defer setup.Close()` from test files:
- worker_test.go: 4 tests
- jobs_test.go: 7 tests
- scan_settings_integration_test.go: 3 tests
- library_browse_test.go: 1 test
- goroutine_leak_test.go: 1 test
- fsnotify_integration_test.go: 1 test

Now setupTestServer()'s t.Cleanup() function properly:
1. Deletes "test" libraries (while DB is still connected)
2. Then calls setup.Close() to close connections

This ensures all test libraries are cleaned up, leaving a clean database
after `make test-integration` completes.

Files changed:
- cmd/server/tests/worker_test.go: Removed 4 defer calls
- cmd/server/tests/jobs_test.go: Removed 7 defer calls
- cmd/server/tests/scan_settings_integration_test.go: Removed 3 defer calls
- cmd/server/tests/library_browse_test.go: Removed 1 defer call
- cmd/server/tests/goroutine_leak_test.go: Removed 1 defer call
- cmd/server/tests/fsnotify_integration_test.go: Removed 1 defer call
2026-03-24 20:55:37 -04:00

318 lines
8.9 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)
}
// Submit multiple concurrent jobs
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,
}
err = services.WorkerInstance.EnqueueJob(job)
require.NoError(t, err, "Failed to enqueue set folders job")
}
// Wait for all jobs to process
time.Sleep(3 * time.Second)
// 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
}