package main import ( "bytes" "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type ScannerIntegrationTestSuite struct { suite.Suite setup *TestServerSetup } func (s *ScannerIntegrationTestSuite) SetupSuite() { s.setup = setupTestServer(s.T()) } func (s *ScannerIntegrationTestSuite) TearDownSuite() { s.setup.Close() } func (s *ScannerIntegrationTestSuite) TestScanProgress_TracksStatistics() { token := s.setup.Token createLibReq := map[string]interface{}{ "name": "Scan Test Library", "description": "Test library for scan progress", "type": "ebooks", } createLibBody, _ := json.Marshal(createLibReq) createLibURL := s.setup.Server.URL + "/api/libraries" createLibReqHTTP, _ := http.NewRequest("POST", createLibURL, bytes.NewBuffer(createLibBody)) createLibReqHTTP.Header.Set("Content-Type", "application/json") createLibReqHTTP.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} createLibResp, err := client.Do(createLibReqHTTP) require.NoError(s.T(), err) require.Equal(s.T(), http.StatusCreated, createLibResp.StatusCode) var createLibResponse map[string]interface{} err = json.NewDecoder(createLibResp.Body).Decode(&createLibResponse) require.NoError(s.T(), err) _ = createLibResp.Body.Close() libraryID, ok := createLibResponse["id"].(string) require.True(s.T(), ok, "library_id should be string") require.NotEmpty(s.T(), libraryID, "library_id should not be empty") folderURL := fmt.Sprintf("%s/api/libraries/%s/folders", s.setup.Server.URL, libraryID) folderReq := map[string]interface{}{ "folder_path": "/app/uploads", } folderBody, _ := json.Marshal(folderReq) req, _ := http.NewRequest("POST", folderURL, bytes.NewBuffer(folderBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) require.NoError(s.T(), err) _ = resp.Body.Close() require.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Folder creation should succeed") scanURL := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) scanReq, _ := http.NewRequest("POST", scanURL, nil) scanReq.Header.Set("Authorization", "Bearer "+token) scanResp, err := client.Do(scanReq) require.NoError(s.T(), err) require.Equal(s.T(), http.StatusAccepted, scanResp.StatusCode) var scanResponse map[string]interface{} err = json.NewDecoder(scanResp.Body).Decode(&scanResponse) require.NoError(s.T(), err) _ = scanResp.Body.Close() jobID, ok := scanResponse["job_id"].(string) require.True(s.T(), ok, "job_id should be string") require.NotEmpty(s.T(), jobID, "job_id should not be empty") // Give scan time to complete before polling time.Sleep(3 * time.Second) var lastProgress float64 var lastFilesScanned, lastNewItems, lastErrors int gotProgressUpdate := false for i := 0; i < 30; i++ { if i > 0 { time.Sleep(1 * time.Second) } statusURL := fmt.Sprintf("%s/api/scanner/status/%s", s.setup.Server.URL, jobID) statusReq, _ := http.NewRequest("GET", statusURL, nil) statusReq.Header.Set("Authorization", "Bearer "+token) statusResp, err := client.Do(statusReq) require.NoError(s.T(), err) if statusResp.StatusCode == http.StatusNotFound { _ = statusResp.Body.Close() if gotProgressUpdate { break } continue } var status map[string]interface{} err = json.NewDecoder(statusResp.Body).Decode(&status) _ = statusResp.Body.Close() require.NoError(s.T(), err) if _, hasError := status["error"]; hasError { continue } assert.Contains(s.T(), status, "files_scanned") assert.Contains(s.T(), status, "new_items") assert.Contains(s.T(), status, "errors") progressFloat, ok := status["progress"].(float64) require.True(s.T(), ok, "progress should be float64") progress := progressFloat filesScannedFloat, ok := status["files_scanned"].(float64) require.True(s.T(), ok, "files_scanned should be float64") filesScanned := int(filesScannedFloat) newItemsFloat, ok := status["new_items"].(float64) require.True(s.T(), ok, "new_items should be float64") newItems := int(newItemsFloat) errorsFloat, ok := status["errors"].(float64) require.True(s.T(), ok, "errors should be float64") errors := int(errorsFloat) assert.GreaterOrEqual(s.T(), progress, lastProgress) lastProgress = progress gotProgressUpdate = true assert.GreaterOrEqual(s.T(), filesScanned, lastFilesScanned) lastFilesScanned = filesScanned assert.GreaterOrEqual(s.T(), newItems, lastNewItems) assert.GreaterOrEqual(s.T(), errors, lastErrors) if status["status"] == "completed" || status["status"] == "failed" { break } } if gotProgressUpdate { assert.Equal(s.T(), 1.0, lastProgress) assert.GreaterOrEqual(s.T(), lastFilesScanned, 0) } } func (s *ScannerIntegrationTestSuite) TestScanProgress_BatchingWorks() { token := s.setup.Token createLibReq := map[string]interface{}{ "name": "Batch Test Library", "description": "Test library for batching", "type": "ebooks", } createLibBody, _ := json.Marshal(createLibReq) createLibURL := s.setup.Server.URL + "/api/libraries" createLibReqHTTP, _ := http.NewRequest("POST", createLibURL, bytes.NewBuffer(createLibBody)) createLibReqHTTP.Header.Set("Content-Type", "application/json") createLibReqHTTP.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} createLibResp, err := client.Do(createLibReqHTTP) require.NoError(s.T(), err) var createLibResponse map[string]interface{} err = json.NewDecoder(createLibResp.Body).Decode(&createLibResponse) require.NoError(s.T(), err) _ = createLibResp.Body.Close() libraryID, ok := createLibResponse["id"].(string) require.True(s.T(), ok, "library_id should be string") require.NotEmpty(s.T(), libraryID, "library_id should not be empty") folderURL := fmt.Sprintf("%s/api/libraries/%s/folders", s.setup.Server.URL, libraryID) folderReq := map[string]interface{}{ "folder_path": "/app/uploads", } folderBody, _ := json.Marshal(folderReq) req, _ := http.NewRequest("POST", folderURL, bytes.NewBuffer(folderBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) require.NoError(s.T(), err) _ = resp.Body.Close() scanURL := fmt.Sprintf("%s/api/libraries/%s/scan", s.setup.Server.URL, libraryID) scanReq, _ := http.NewRequest("POST", scanURL, nil) scanReq.Header.Set("Authorization", "Bearer "+token) scanResp, err := client.Do(scanReq) require.NoError(s.T(), err) var scanResponse map[string]interface{} err = json.NewDecoder(scanResp.Body).Decode(&scanResponse) require.NoError(s.T(), err) _ = scanResp.Body.Close() jobID, ok := scanResponse["job_id"].(string) require.True(s.T(), ok, "job_id should be string") require.NotEmpty(s.T(), jobID, "job_id should not be empty") updateCount := 0 previousFilesScanned := -1 for i := 0; i < 20; i++ { if i > 0 { time.Sleep(500 * time.Millisecond) } statusURL := fmt.Sprintf("%s/api/scanner/status/%s", s.setup.Server.URL, jobID) statusReq, _ := http.NewRequest("GET", statusURL, nil) statusReq.Header.Set("Authorization", "Bearer "+token) statusResp, err := client.Do(statusReq) require.NoError(s.T(), err) if statusResp.StatusCode == http.StatusNotFound { _ = statusResp.Body.Close() break } var status map[string]interface{} err = json.NewDecoder(statusResp.Body).Decode(&status) require.NoError(s.T(), err) _ = statusResp.Body.Close() if _, hasError := status["error"]; hasError { continue } filesScannedFloat, ok := status["files_scanned"].(float64) require.True(s.T(), ok, "files_scanned should be float64") filesScanned := int(filesScannedFloat) if filesScanned != previousFilesScanned { updateCount++ previousFilesScanned = filesScanned } if status["status"] == "completed" || status["status"] == "failed" { break } } assert.Less(s.T(), updateCount, 50) } func TestScannerIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ScannerIntegrationTestSuite)) }