Files
bookhoard/cmd/server/tests/scanner_integration_test.go
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

270 lines
8.0 KiB
Go

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))
}