Files
bookhoard/cmd/server/tests/processing_issues_test.go
T
john-okeefe 96d05886ef 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

525 lines
18 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestProcessingIssuesListAuthentication tests authentication and authorization for list endpoint
func TestProcessingIssuesListAuthentication(t *testing.T) {
setup := setupTestServer(t)
// Create a test library
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test Auth Library", "manga")
t.Run("GET /api/libraries/:id/issues/list - No authentication", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("GET /api/libraries/:id/issues/list - Non-admin user forbidden", func(t *testing.T) {
token := setup.RegularToken
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
})
t.Run("GET /api/libraries/:id/issues/list - Admin user authorized", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
})
}
// TestProcessingIssuesListResponseStructure tests the response structure of list endpoint
func TestProcessingIssuesListResponseStructure(t *testing.T) {
setup := setupTestServer(t)
// Create a test library
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test Structure Library", "comics")
t.Run("Returns JSON array for empty library", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response []map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.IsType(t, []map[string]interface{}{}, response)
assert.Equal(t, 0, len(response), "Empty library should return empty array")
})
t.Run("Has correct content type", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
})
}
// TestProcessingIssuesListInputValidation tests input validation for list endpoint
func TestProcessingIssuesListInputValidation(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
testCases := []struct {
name string
libraryID string
expectedStatus int
description string
}{
{
name: "Invalid UUID format",
libraryID: "not-a-uuid",
expectedStatus: http.StatusBadRequest,
description: "Should reject malformed UUID",
},
{
name: "Empty UUID",
libraryID: "",
expectedStatus: http.StatusNotFound,
description: "Should return 404 for empty ID",
},
{
name: "UUID with extra path traversal",
libraryID: "00000000-0000-0000-0000-000000000000/../../etc",
expectedStatus: http.StatusBadRequest,
description: "Should reject path traversal attempts",
},
{
name: "Nonexistent library UUID",
libraryID: "00000000-0000-0000-0000-000000000000",
expectedStatus: http.StatusOK,
description: "Should return empty array for valid but nonexistent UUID",
},
{
name: "Special characters in UUID",
libraryID: "'; DROP TABLE processing_issues; --",
expectedStatus: http.StatusBadRequest,
description: "Should reject SQL injection attempts",
},
{
name: "Very long UUID",
libraryID: "00000000-0000-0000-0000-000000000000-extra-suffix",
expectedStatus: http.StatusBadRequest,
description: "Should reject overly long IDs",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+tc.libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, tc.expectedStatus, rec.Code, tc.description)
// For successful requests, verify response structure
if tc.expectedStatus == http.StatusOK {
var response []map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.IsType(t, []map[string]interface{}{}, response)
}
})
}
}
// TestProcessingIssueStatsAuthentication tests authentication and authorization for stats endpoint
func TestProcessingIssueStatsAuthentication(t *testing.T) {
setup := setupTestServer(t)
// Create a test library
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test Stats Auth Library", "manga")
t.Run("GET /api/libraries/:id/issues/stats - No authentication", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("GET /api/libraries/:id/issues/stats - Non-admin user forbidden", func(t *testing.T) {
token := setup.RegularToken
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
})
t.Run("GET /api/libraries/:id/issues/stats - Admin user authorized", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
})
}
// TestProcessingIssueStatsResponseStructure tests the response structure of stats endpoint
func TestProcessingIssueStatsResponseStructure(t *testing.T) {
setup := setupTestServer(t)
// Create a test library
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test Stats Structure Library", "comics")
t.Run("Returns correct structure for empty library", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
// Verify required fields
assert.Contains(t, response, "error_count", "Should have error_count field")
assert.Contains(t, response, "warning_count", "Should have warning_count field")
assert.Contains(t, response, "info_count", "Should have info_count field")
// Verify all counts are zero for empty library
assert.Equal(t, float64(0), response["error_count"], "error_count should be 0")
assert.Equal(t, float64(0), response["warning_count"], "warning_count should be 0")
assert.Equal(t, float64(0), response["info_count"], "info_count should be 0")
// Verify field types
assert.IsType(t, float64(0), response["error_count"], "error_count should be number")
assert.IsType(t, float64(0), response["warning_count"], "warning_count should be number")
assert.IsType(t, float64(0), response["info_count"], "info_count should be number")
})
t.Run("Has correct content type", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
})
t.Run("Has exactly 3 fields", func(t *testing.T) {
token := setup.Token
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.Len(t, response, 3, "Should have exactly 3 fields")
})
}
// TestProcessingIssueStatsInputValidation tests input validation for stats endpoint
func TestProcessingIssueStatsInputValidation(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
testCases := []struct {
name string
libraryID string
expectedStatus int
description string
}{
{
name: "Invalid UUID format",
libraryID: "not-a-uuid",
expectedStatus: http.StatusBadRequest,
description: "Should reject malformed UUID",
},
{
name: "Empty UUID",
libraryID: "",
expectedStatus: http.StatusNotFound,
description: "Should return 404 for empty ID",
},
{
name: "UUID with extra path traversal",
libraryID: "00000000-0000-0000-0000-000000000000/../../etc",
expectedStatus: http.StatusBadRequest,
description: "Should reject path traversal attempts",
},
{
name: "Nonexistent library UUID",
libraryID: "00000000-0000-0000-0000-000000000000",
expectedStatus: http.StatusOK,
description: "Should return zero counts for valid but nonexistent UUID",
},
{
name: "Special characters in UUID",
libraryID: "'; DROP TABLE processing_issues; --",
expectedStatus: http.StatusBadRequest,
description: "Should reject SQL injection attempts",
},
{
name: "Very long UUID",
libraryID: "00000000-0000-0000-0000-000000000000-extra-suffix",
expectedStatus: http.StatusBadRequest,
description: "Should reject overly long IDs",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+tc.libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, tc.expectedStatus, rec.Code, tc.description)
// For successful requests, verify response structure
if tc.expectedStatus == http.StatusOK {
var response map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
// Verify all counts are zero for nonexistent library
assert.Equal(t, float64(0), response["error_count"])
assert.Equal(t, float64(0), response["warning_count"])
assert.Equal(t, float64(0), response["info_count"])
}
})
}
}
// TestProcessingIssuesCrossLibraryIsolation tests that different libraries don't interfere
func TestProcessingIssuesCrossLibraryIsolation(t *testing.T) {
setup := setupTestServer(t)
// Create two different libraries with different types
deviceSetup1 := setupDeviceTest(t)
library1ID := deviceSetup1.CreateLibrary(t, "Isolation Test Library 1", "manga")
deviceSetup2 := setupDeviceTest(t)
library2ID := deviceSetup2.CreateLibrary(t, "Isolation Test Library 2", "comics")
token := setup.Token
// Both libraries should return empty arrays (no issues created)
t.Run("Library 1 returns its own empty list", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+library1ID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response []map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.Equal(t, 0, len(response), "Library 1 should have 0 issues")
})
t.Run("Library 2 returns its own empty list", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+library2ID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response []map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.Equal(t, 0, len(response), "Library 2 should have 0 issues")
})
t.Run("Both libraries have independent stats", func(t *testing.T) {
// Get stats for library 1
req1 := httptest.NewRequest("GET", "/api/libraries/"+library1ID+"/issues/stats", nil)
req1.Header.Set("Authorization", "Bearer "+token)
rec1 := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec1, req1)
var stats1 map[string]interface{}
err := json.NewDecoder(rec1.Body).Decode(&stats1)
require.NoError(t, err)
// Get stats for library 2
req2 := httptest.NewRequest("GET", "/api/libraries/"+library2ID+"/issues/stats", nil)
req2.Header.Set("Authorization", "Bearer "+token)
rec2 := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec2, req2)
var stats2 map[string]interface{}
err = json.NewDecoder(rec2.Body).Decode(&stats2)
require.NoError(t, err)
// Both should have zero counts
assert.Equal(t, float64(0), stats1["error_count"])
assert.Equal(t, float64(0), stats1["warning_count"])
assert.Equal(t, float64(0), stats1["info_count"])
assert.Equal(t, float64(0), stats2["error_count"])
assert.Equal(t, float64(0), stats2["warning_count"])
assert.Equal(t, float64(0), stats2["info_count"])
})
}
// TestProcessingIssuesDifferentLibraryTypes tests endpoints with different library types
func TestProcessingIssuesDifferentLibraryTypes(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
libraryTypes := []struct {
name string
libraryType string
}{
{"Ebooks library", "ebooks"},
{"Comics library", "comics"},
{"Manga library", "manga"},
{"Audiobooks library", "audiobooks"},
}
for _, lt := range libraryTypes {
t.Run(lt.name+" - list endpoint works", func(t *testing.T) {
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType)
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response []map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.IsType(t, []map[string]interface{}{}, response)
})
t.Run(lt.name+" - stats endpoint works", func(t *testing.T) {
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test "+lt.name, lt.libraryType)
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]interface{}
err := json.NewDecoder(rec.Body).Decode(&response)
require.NoError(t, err)
assert.Contains(t, response, "error_count")
assert.Contains(t, response, "warning_count")
assert.Contains(t, response, "info_count")
})
}
}
// TestProcessingIssuesInvalidTokens tests with invalid authentication tokens
func TestProcessingIssuesInvalidTokens(t *testing.T) {
setup := setupTestServer(t)
// Create a test library
deviceSetup := setupDeviceTest(t)
libraryID := deviceSetup.CreateLibrary(t, "Test Token Library", "ebooks")
t.Run("List endpoint rejects invalid token", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "Bearer invalid-token-12345")
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("Stats endpoint rejects invalid token", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", "Bearer invalid-token-12345")
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("List endpoint rejects missing bearer prefix", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", setup.Token) // Missing "Bearer " prefix
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("Stats endpoint rejects missing bearer prefix", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/stats", nil)
req.Header.Set("Authorization", setup.Token) // Missing "Bearer " prefix
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("List endpoint rejects empty authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/"+libraryID+"/issues/list", nil)
req.Header.Set("Authorization", "")
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
}