Files
john-okeefe a6700f73e0 fix(tests): handle all Close() and Decode() errors across integration tests
Replace all unhandled resp.Body.Close() calls throughout the test suite:

- Deferred calls: replace 'defer VAR.Body.Close()' with a closure that explicitly
  discards the error via 'defer func(Body io.ReadCloser) { _ = Body.Close() }(VAR.Body)'
- Immediate calls: replace 'VAR.Body.Close()' with '_ = VAR.Body.Close()'

Replace all unhandled json.NewDecoder(VAR.Body).Decode(&x) calls with error capture
and require.NoError assertion. Files using httptest.ResponseRecorder (collections_preview,
processing_issues) use 'err :=' declaration; suite-style tests (scanner_integration,
dashboard_integration) use s.T() instead of t.
2026-04-21 20:33:05 -04:00

342 lines
9.7 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{}
err = json.NewDecoder(createLibResp.Body).Decode(&createLibResponse)
require.NoError(t, err)
_ = 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{}
err = json.NewDecoder(createLibResp.Body).Decode(&createLibResponse)
require.NoError(t, err)
_ = 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
}