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) defer setup.Close() 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) defer setup.Close() 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) defer setup.Close() 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) defer setup.Close() 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 }